/**
* 火山引擎 DataRangers 埋点 SDK 适配模块(uni-app)
*
* 基于 @datarangers/sdk-javascript,适配 uni-app 运行环境。
* 主要功能:
* - 初始化埋点 SDK 并配置应用信息
* - 页面浏览(PV)自动追踪,通过 uni-crazy-router afterEach 钩子实现
* - 自定义事件上报(trackEvent)
* - 事件缓冲队列,支持持久化存储,避免小程序切换后台时事件丢失
* - 用户身份自动同步(基于 userStore)
*/
/**
* 火山引擎 DataRangers 埋点 SDK 适配模块(uni-app)
*
* 基于 @datarangers/sdk-javascript,适配 uni-app 运行环境。
* 主要功能:
* - 初始化埋点 SDK 并配置应用信息
* - 页面浏览(PV)自动追踪,通过 uni-crazy-router afterEach 钩子实现
* - 自定义事件上报(trackEvent)
* - 事件缓冲队列,支持持久化存储,避免小程序切换后台时事件丢失
* - 用户身份自动同步(基于 userStore)
*/
import DataRangers, { type EventParams } from '@datarangers/sdk-javascript'
import { afterEach } from 'uni-crazy-router'
import { useUserStore } from '@/store/modules/user'
// ======================== 配置与状态 ========================
/** 从环境变量读取火山应用 ID,.env 中 VITE_VOLC_APP_ID = 20014085 */
const analyticsAppId = Number(import.meta.env.VITE_VOLC_APP_ID || 0)
/** 埋点开关:应用 ID 有效且大于 0 时启用 */
const analyticsEnabled = Number.isFinite(analyticsAppId) && analyticsAppId > 0
/** SDK 是否已完成初始化 */
let analyticsInitialized = false
/** 当前已同步到 SDK 的用户唯一 ID,用于避免重复调用 config */
let currentUserUniqueId = ''
// ======================== 事件缓冲队列 ========================
/** 缓冲事件结构 */
type BufferedEvent = {
event: string
params: EventParams
timestamp: number
}
/** 内存中的缓冲事件队列 */
const bufferedEvents: BufferedEvent[] = []
/** 缓冲事件持久化存储 key */
const bufferStorageKey = '__VOLC_DR_EVENT_BUFFER__'
/** 已去重的 trackId 持久化存储 key */
const bufferTrackIdStorageKey = '__VOLC_DR_TRACK_ID_SET__'
/** 已入队的 trackId 集合,用于事件去重 */
const bufferedTrackIds = new Set<string>()
/** 缓冲队列最大容量,达到后自动 flush */
const bufferMaxSize = 20
// ======================== 页面信息获取(uni-app 适配) ========================
/**
* 从当前页面 URL 中提取 distributorId 参数
* uni-app 下通过 getCurrentPages() 获取页面栈
*/
function getDistributorId() {
try {
const pages = getCurrentPages()
if (pages.length > 0) {
const page = pages[pages.length - 1]
const url = '/' + page.route
const searchIndex = url.indexOf('?')
if (searchIndex > -1) {
const search = url.slice(searchIndex + 1)
const match = search.match(/(?:^|&)distributorId=([^&]*)/)
if (match) return match[1]
}
}
} catch (e) {}
return ''
}
/**
* 获取当前页面路径(不含查询参数)
* 例:/pages/home/index
*/
function getCurrentPagePath() {
try {
const pages = getCurrentPages()
if (pages.length > 0) {
return '/' + (pages[pages.length - 1].route || '')
}
} catch (e) {}
return ''
}
/**
* 获取当前页面完整路径(含查询参数)
* 例:/pages/home/index?id=1&type=2
*/
function getCurrentPageFullPath() {
try {
const pages = getCurrentPages()
if (pages.length > 0) {
const page = pages[pages.length - 1]
const route = page.route || ''
const options = (page as any).options || (page as any).$page?.options || {}
const query = Object.keys(options).map(k => `${k}=${options[k]}`).join('&')
return query ? `/${route}?${query}` : `/${route}`
}
} catch (e) {}
return ''
}
/**
* 构造所有事件通用的基础参数
* 包含页面信息、来源、分销商 ID、用户 ID
*/
function getBaseEventParams() {
const userStoreInstance = useUserStore()
const userId = userStoreInstance.userInfo?.id ? String(userStoreInstance.userInfo?.id) : ''
return {
page_title: getCurrentPagePath(),
page_url: getCurrentPageFullPath(),
page_path: getCurrentPagePath(),
page_query: '',
referrer: '',
distributor_id: getDistributorId(),
user_id: userId
}
}
// ======================== 用户身份同步 ========================
/**
* 将用户身份信息同步到 DataRangers SDK
* 仅当 userId 变化时才调用,避免重复设置
*/
function syncUserContext() {
if (!analyticsEnabled || !analyticsInitialized) {
return
}
const userStoreInstance = useUserStore()
const userUniqueId = userStoreInstance.userInfo?.id ? String(userStoreInstance.userInfo?.id) : ''
if (!userUniqueId || userUniqueId === currentUserUniqueId) {
return
}
currentUserUniqueId = userUniqueId
DataRangers.config({
user_unique_id: userUniqueId,
user_id: userUniqueId
})
}
// ======================== 缓冲队列持久化 ========================
/** 将内存中的缓冲事件和 trackId 持久化到 Storage,防止小程序销毁时丢失 */
function persistBuffer() {
try {
uni.setStorageSync(bufferStorageKey, JSON.stringify(bufferedEvents))
uni.setStorageSync(bufferTrackIdStorageKey, JSON.stringify(Array.from(bufferedTrackIds)))
} catch (e) {}
}
/** 从 Storage 恢复缓冲事件和 trackId,用于小程序重启后续传 */
function restoreBuffer() {
try {
const raw = uni.getStorageSync(bufferStorageKey)
if (raw) {
const list = JSON.parse(raw)
if (Array.isArray(list) && list.length > 0) {
list.forEach((item: any) => {
if (item && typeof item.event === 'string') {
bufferedEvents.push({
event: item.event,
params: item.params || {},
timestamp: typeof item.timestamp === 'number' ? item.timestamp : Date.now()
})
}
})
}
}
} catch (e) {}
try {
const rawTrackIds = uni.getStorageSync(bufferTrackIdStorageKey)
if (rawTrackIds) {
const list = JSON.parse(rawTrackIds)
if (Array.isArray(list) && list.length > 0) {
list.forEach((id: string) => {
if (typeof id === 'string' && id) {
bufferedTrackIds.add(id)
}
})
}
}
} catch (e) {}
}
/** 清除 Storage 中的缓冲数据 */
function clearPersistedBuffer() {
try {
uni.removeStorageSync(bufferStorageKey)
uni.removeStorageSync(bufferTrackIdStorageKey)
} catch (e) {}
}
// ======================== 缓冲队列刷新 ========================
/**
* 将缓冲队列中的事件批量上报到 DataRangers
* @param useBeacon - 为 true 时使用 beconEvent(页面卸载场景,确保数据发出)
*/
function flushBufferedEvents(useBeacon = false) {
if (!analyticsEnabled || !analyticsInitialized) {
return
}
if (bufferedEvents.length < 1) {
clearPersistedBuffer()
return
}
syncUserContext()
const list = bufferedEvents.splice(0, bufferedEvents.length)
bufferedTrackIds.clear()
clearPersistedBuffer()
if (useBeacon) {
// 页面卸载时使用 beacon 方式,尽量保证数据送达
list.forEach(({ event, params }) => {
DataRangers.beconEvent(event, params)
})
return
}
// 批量上报:[[eventName, params, timestamp], ...]
DataRangers.event(
list.map(({ event, params, timestamp }) => [event, params, timestamp])
)
}
/**
* 将事件加入缓冲队列
* - 通过 track_id 去重,同一 trackId 的事件只记录一次
* - 入队后持久化到 Storage
* - 队列达到上限时自动 flush
*/
function enqueueEvent(eventName: string, params: EventParams = {}) {
if (!analyticsEnabled || !analyticsInitialized || !eventName) {
return
}
const trackId = typeof params.track_id === 'string' ? params.track_id : ''
if (trackId && bufferedTrackIds.has(trackId)) {
return
}
syncUserContext()
bufferedEvents.push({
event: eventName,
params: {
...getBaseEventParams(),
...params
},
timestamp: Date.now()
})
if (trackId) {
bufferedTrackIds.add(trackId)
}
persistBuffer()
if (bufferedEvents.length >= bufferMaxSize) {
flushBufferedEvents(false)
}
}
// ======================== 对外导出的核心方法 ========================
/**
* 初始化 DataRangers 埋点 SDK
* - 配置应用 ID、渠道、调试模式
* - 设置平台为 h5,应用名为 hssc-shop
* - 启动 SDK 并恢复历史缓冲事件
* - 在 main.ts 中调用
*/
export function initAnalytics() {
if (!analyticsEnabled || analyticsInitialized) {
return
}
console.log('开始埋点')
DataRangers.init({
app_id: analyticsAppId,
channel: (import.meta.env.VITE_VOLC_CHANNEL as 'cn' | 'sg' | 'va') || 'cn',
channel_domain: import.meta.env.VITE_VOLC_CHANNEL_DOMAIN || undefined,
log: import.meta.env.VITE_VOLC_ENABLE_DEBUG === 'true',
enable_debug: import.meta.env.VITE_VOLC_ENABLE_DEBUG === 'true',
enable_spa: true,
disable_auto_pv: true // 禁用自动 PV,由 trackPageView 手动上报
})
DataRangers.config({
platform: 'h5',
app_name: 'hssc-shop'
})
DataRangers.start()
analyticsInitialized = true
restoreBuffer() // 恢复上次未上报的缓冲事件
bindBufferFlushLifecycle() // 绑定页面生命周期,确保页面隐藏时 flush
flushBufferedEvents(false) // 立即上报恢复的事件
syncUserContext() // 同步当前用户身份
}
/**
* 上报自定义事件
* @param eventName - 事件名称
* @param params - 事件参数,会自动合并页面基础参数
*/
export function trackEvent(eventName: string, params: EventParams = {}) {
if (!analyticsEnabled || !analyticsInitialized || !eventName) {
return
}
syncUserContext()
DataRangers.event(eventName, {
...getBaseEventParams(),
...params
})
}
/**
* 上报页面浏览事件(PV)
* 适配 uni-crazy-router 的路由钩子参数格式
* @param to - 目标路由信息 { url, search, query }
* @param from - 来源路由信息(首次进入时为 null)
*/
export function trackPageView(to: { url: string; search?: string; query?: object | null }, from?: { url: string; search?: string; query?: object | null } | null) {
const routePath = '/' + (to.url || '')
const routeFullPath = to.search ? routePath + '?' + to.search : routePath
const fromRoutePath = from ? '/' + (from.url || '') : ''
const fromRouteFullPath = from && from.search ? fromRoutePath + '?' + from.search : fromRoutePath
trackEvent('page_view', {
route_name: to.url || '',
route_path: routePath,
route_full_path: routeFullPath,
from_route_name: from?.url || '',
from_route_path: fromRouteFullPath
})
}
/** 手动触发缓冲队列刷新 */
export function flushAnalyticsBuffer() {
flushBufferedEvents(false)
}
/**
* 注册路由追踪:在每次路由跳转后自动上报 PV 事件
* 通过 uni-crazy-router 的 afterEach 钩子实现
* 在 main.ts 中调用
*/
export function setupRouteTracking() {
afterEach((to, from) => {
trackPageView(to, from)
})
}
// ======================== 页面生命周期绑定 ========================
/**
* 绑定页面隐藏/卸载时的缓冲刷新逻辑
* H5 环境下监听 visibilitychange / pagehide / beforeunload
* 小程序端需要通过 App.vue 的 onHide 生命周期手动调用 flushAnalyticsBuffer()
*/
function bindBufferFlushLifecycle() {
// #ifdef H5
try {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
flushBufferedEvents(true)
}
})
window.addEventListener('pagehide', () => {
flushBufferedEvents(true)
})
window.addEventListener('beforeunload', () => {
flushBufferedEvents(true)
})
} catch (e) {}
// #endif
}
env配置文件:
# 火山引擎 DataRangers
VITE_VOLC_APP_ID = xxxxxx
VITE_VOLC_CHANNEL_DOMAIN=https://xxxxx.com
VITE_VOLC_ENABLE_DEBUG=false
main.ts:
import { createSSRApp } from 'vue'
import App from './App.vue'
import { setupStore } from '@/store'
import { setupRouter } from "@/utils/router"
//引入埋点工具并调用
import { initAnalytics, setupRouteTracking } from '@/utils/analytics111'
export function createApp() {
const app = createSSRApp(App)
// 注册状态管理
setupStore(app)
setupRouter(app)
initAnalytics()
setupRouteTracking()
return {
app
}
}
APP.vue:
<template></template>
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { trackEvent } from '@/utils/analytics11111'
onLaunch((option : any) => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
// 启动埋点
trackEvent('app_launch', {title: '开始启动'})
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
</style>