feat: add frontend analytics tracking
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user