uniapp封装火山引擎 DataRangers 埋点 SDK

/**

* 火山引擎 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>
相关推荐
阿坤带你走近大数据3 小时前
Apache Hop的详细介绍
apache
2501_915909063 小时前
iOS IPA文件反编译与打包操作方法详解
android·ios·小程序·https·uni-app·iphone·webview
Greg_Zhong1 天前
火山引擎免费模型调用遇到的问题及记录
火山引擎·豆包免费模型调用的坑及总结·还有免费额度,为什么走付费节点
2501_915921431 天前
uni-app 上架 iOS 的完整流程(无需依赖 Mac)
android·macos·ios·小程序·uni-app·iphone·webview
车车不吃香菇1 天前
使用java实现即梦文生视频、图生视频,火山引擎「即梦 AI - 视频生成 3.0 Pro」调用 Demo(原生 HTTP 签名版)。
人工智能·火山引擎
于先生吖2 天前
前后端分离二手商城开发,质检登记、回收回款整套业务源码部署教程
java·开发语言·uni-app
就叫_这个吧2 天前
servlet整合tomcat项目启动报错解决,org.apache.tomcat.util.descriptor.web.WebXml.setVersion
java·servlet·tomcat·apache
云器科技2 天前
Apache Iceberg-cpp:原生性能架构与演进路线
架构·apache
Par@ish2 天前
Ubuntu Apache日志存储周期变更
linux·ubuntu·apache