Skip to content

配方 · 埋点与监测

参考实现:examples/team-video-vue/composables/useTeamAnalytics.ts

需求:要看板上有起播成功率、起播耗时、卡顿率、错误分布。


SDK 不上报,只给数据

useTeamAnalytics 全文就是一个依赖注入的空壳:

ts
export interface TeamAnalyticsAdapter {
  track(eventName: string, data?: Record<string, unknown>): void
  impression?(name: string, data?: Record<string, unknown>): void
}

const noopAdapter: TeamAnalyticsAdapter = {
  track(eventName, data) {
    if (import.meta.env?.DEV) console.log('[team-video-analytics]', eventName, data)
  },
}

应用启动时注入你真正的实现(Faro / GrowingIO / 自研):

ts
provideTeamAnalytics({
  track: (name, data) => myTracker.send(name, data),
})

默认是 no-op —— 不注入就什么都不发,不会偷偷往某个域名打点。


四个核心指标怎么算

① 起播成功率

js
let started = false
onReady(() => { attempted = true })
onPlaying(() => { if (!started) { started = true; track('play_success') } })
onError(({ code }) => { if (!started) track('play_fail', { code }) })

分母是「用户想看」的次数,分子是真的看到画面的次数。

playing 而不是 play

play 只表示发起播放,此时可能还在缓冲、甚至马上就会失败。 playing 才表示画面真的动了

play 算成功率会显著虚高。

② 起播耗时

js
let t0 = 0
// 用户点击 / 页面加载那一刻
function onIntent() { t0 = performance.now() }

onPlaying(() => {
  if (t0) { track('play_latency', { ms: performance.now() - t0 }); t0 = 0 }
})

起点要选用户产生意图的时刻,不是 ready。用户感知的等待是从他点下去开始的。

③ 卡顿率 ⭐

stalled,不要用 waiting:

js
onStalled(({ phase, position, durationMs }) => {
  if (phase === 'end') {
    totalStallMs += durationMs
    stallCount += 1
    track('stall', { position, durationMs })
  }
})

// 卡顿率 = 卡顿总时长 / 播放总时长
waiting/playingstalled
带时长❌ 得自己掐表durationMs
冻帧型卡顿抓不到✅ 主动轮询 currentTime
后台切走照发,污染指标不计

「冻帧型」是指画面卡死但 xgplayer 根本不发 waiting 的情况。 只用 waiting 做监控,这类卡顿在你的看板上是不存在的 —— 而用户投诉的往往正是它。

「后台不计」也很关键:用户切到别的 App,currentTime 本来就停, 那不是卡顿。计进去会让移动端的卡顿率虚高一大截。

④ 错误分布

category 聚合,不要按 code:

js
onError(({ code, category, retryable }) => {
  track('player_error', { code, category, retryable })
})

code 会随版本增删(比如 ADR-034 一次删了 12 个),按 code 做的看板会断层。 category 只有 6 个且稳定:manifest / media / network / auth / env / autoplay

日常看 category 趋势,排查时才下钻到 code


合规过滤放在你这一侧

SDK 不做隐私门控。所有契约事件都无条件发给你。

ts
// ✅ 正确:在你自己的上报函数里拦
function report(name: string, payload: unknown) {
  if (!consent.analytics) return   // consent 是你的状态,不是 SDK 的
  analytics.track(name, payload)
}

// 绑在组件上:<VideoPlayerFrame @stalled="(p) => report('video_stalled', p)" />

为什么不是 SDK 拦

SDK 从不发出任何网络请求(红线禁引 HTTP 库)—— 事件只是交给同一个页面里的你, 数据从没离开过浏览器。真正需要合规判断的是「你要不要把它发给埋点平台」, 那一步只发生在你这里,consent 状态也在你手上。

1.8.0 起 consent 字段已删除

早期版本有 config.consent,传 { analytics: false } 会让 player-core 不 emit stalled。 那道门控已按 ADR-040 移除,字段也一并删了。

为什么改:stalled 是双用途事件 —— phase: 'start' 驱动 loading UI,phase: 'end' 才是埋点数据。 拿埋点开关掐整个事件,等于把 loading 也掐了。而冻帧型卡死(画面卡死但 xgplayer 连 waiting 都不发)时 stalled 是你能拿到的唯一信号 —— 门控开着就意味着用户盯着 一张冻住的画面,你什么都收不到。

要改什么:继续传 consent 不会报错(会被忽略),但 TS 会编译失败,删掉即可; 把过滤逻辑挪进你自己的上报函数,像上面那样。


事件名集中管理

参考实现把埋点事件名放在 constants.ts:

ts
export const ANALYTICS_EVENTS = {
  LIVE_CARD_IMPRESSION: '...',
  LIVE_CARD_CLICK: '...',
  QUALITY_LOGIN_REQUIRED: '...',
} as const

散在各组件里写字符串,过半年就会出现 play_success / playSuccess / play-success 三个版本同时存在,看板上是三条线。


别漏掉「安静失败」的场景

首页预览卡通常会关掉所有错误 UI(见预览卡配方)。 UI 关了,埋点不能跟着关:

js
function handleError(payload) {
  analytics.track('preview_card_error', { code: payload.code })
  // 不弹 toast,但数据要有
}

否则首页预览的失败率永远是 0 —— 不是因为没失败,是因为没人看。


相关