feat: add frontend analytics tracking

This commit is contained in:
2026-05-17 10:18:56 +08:00
parent 3decff526a
commit 1016111d19
11 changed files with 583 additions and 10 deletions
+124
View File
@@ -0,0 +1,124 @@
import { post } from './request.js'
const QUEUE_KEY = 'analytics_queue'
const ANON_KEY = 'analytics_anonymous_id'
const MAX_QUEUE = 100
const FLUSH_SIZE = 20
const FLUSH_INTERVAL = 10000
let queue = []
let sessionId = ''
let pageEnterAt = {}
let flushTimer = null
let initialized = false
const now = () => Date.now()
const uuid = (prefix) => `${prefix}_${now()}_${Math.random().toString(36).slice(2, 10)}`
export const getAnonymousId = () => {
let id = uni.getStorageSync(ANON_KEY)
if (!id) {
id = uuid('anon')
uni.setStorageSync(ANON_KEY, id)
}
return id
}
const loadQueue = () => {
const saved = uni.getStorageSync(QUEUE_KEY)
queue = Array.isArray(saved) ? saved.slice(-MAX_QUEUE) : []
}
const saveQueue = () => {
uni.setStorageSync(QUEUE_KEY, queue.slice(-MAX_QUEUE))
}
const getDeviceInfo = () => {
try {
const system = uni.getSystemInfoSync()
return {
platform: system.platform,
model: system.model,
system: system.system,
version: system.version,
SDKVersion: system.SDKVersion
}
} catch (error) {
return { platform: 'unknown' }
}
}
const safeProperties = (properties = {}) => {
const blocked = ['token', 'access_token', 'refresh_token', 'phone', 'smsCode', 'content', 'fullContent']
return Object.keys(properties || {}).reduce((acc, key) => {
if (!blocked.includes(key)) acc[key] = properties[key]
return acc
}, {})
}
export const initAnalytics = () => {
if (initialized) return
initialized = true
sessionId = uuid('session')
loadQueue()
track('app_launch', {}, { eventType: 'app' })
if (!flushTimer) {
flushTimer = setInterval(() => flush(), FLUSH_INTERVAL)
}
}
export const track = (eventName, properties = {}, options = {}) => {
if (!eventName) return
queue.push({
eventName,
eventType: options.eventType || eventName.split('_')[0] || 'custom',
pagePath: options.pagePath || '',
referrerPath: options.referrerPath || '',
properties: safeProperties(properties),
durationMs: options.durationMs,
occurredAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
})
queue = queue.slice(-MAX_QUEUE)
saveQueue()
if (queue.length >= FLUSH_SIZE) {
flush()
}
}
export const trackPageView = (pagePath, properties = {}) => {
pageEnterAt[pagePath] = now()
track('page_view', properties, { eventType: 'page', pagePath })
}
export const trackPageLeave = (pagePath, properties = {}) => {
const durationMs = pageEnterAt[pagePath] ? now() - pageEnterAt[pagePath] : undefined
delete pageEnterAt[pagePath]
track('page_leave', properties, { eventType: 'page', pagePath, durationMs })
}
export const flush = async () => {
if (!queue.length || !sessionId) return
const sending = queue.slice(0, 50)
try {
await post('/analytics/events/batch', {
anonymousId: getAnonymousId(),
sessionId,
deviceInfo: getDeviceInfo(),
events: sending
})
queue = queue.slice(sending.length)
saveQueue()
} catch (error) {
saveQueue()
}
}
export default {
initAnalytics,
track,
trackPageView,
trackPageLeave,
flush,
getAnonymousId
}