同一场周会,两个人各拿一张报表,同一个"周新增"差了 6%:一个按注册完成时间,一个按首次登录时间;一个含三个内部测试账号,一个不含。会开到一半,议题从增长转成"到底谁的数对"。
根因不在数据,在口径没有 owner。口径只存在于某个人的 SQL 里,就没有维护者、没有版本、没有变更记录,谁改谁负责都说不清。
1. 三种埋点与它们的完整性
| 类型 | 完整性风险 | 成本 | 适合 |
|---|---|---|---|
| 前端事件 | 网络失败、SDK 阻塞、隐私限制导致丢报 | 低,改动快 | 交互行为、曝光与点击 |
| 后端事件 | 只在服务端能观测到,但业务分支易漏 | 中,需服务发版 | 交易、状态流转、服务端校验 |
| 日志派生 | 依赖日志字段稳定,解析易随格式漂移 | 低边际成本,高维护 | 已有日志覆盖的系统行为 |
判据:事实发生在哪一侧。点击在前端,支付成功在后端;把支付成功做成前端事件,等于用"看到成功页"代表"已支付"——弱网下两者必然分叉。
同一个指标跨端拼接时,还要处理"同一动作被两侧各记一次"的重复。常见做法是前端只记意图、后端记事实,指标计算以后端为准。
2. 口径必须写到什么程度
| 要素 | 必须回答 | 只写名字会怎样 |
|---|---|---|
| 分子 | 计什么、按什么条件筛 | 双方各算一版,差在排除条件 |
| 分母 | 是否存在、范围如何界定 | 比率型指标分母漂移,趋势假性上升 |
| 时间窗 | 自然日/滚动 7 日/按事件时间 | 跨零点的行为被切到两天 |
| 去重键 | 按 user_id、device_id 还是订单号 | 多端用户被算成两个 |
"周新增用户"这六个字,至少缺五项。只写名字的口径有个特征:它能被两种都正确的方式实现,而这两种实现在任何一次对话里都无法证伪。
3. 三方所有权与"三不管"
| 角色 | 负责 | 常见空档 |
|---|---|---|
| 产品 | 定义要回答什么问题、口径的业务含义 | 定义完就不再看实现 |
| 数据 | 口径实现、校验、文档与版本 | 不掌握埋点是否漏报 |
| 研发 | 埋点上报、字段含义、变更通知 | 认为口径与己无关 |
典型后果:产品改了语义("激活"从"首次登录"改成"首次核心行为"),研发按新语义改埋点,数据侧口径没动,于是报表在某版本悄悄换了一次定义,没人发现,直到半年后同比失真。
可行解是一个 owner + 两个协作者:owner 唯一(通常数据侧),产品确认业务含义,研发负责上报与变更通知。owner 唯一才有唯一责任人。
4. 埋点的生命周期
| 阶段 | 必须做的事 | 不做会怎样 |
|---|---|---|
| 新增 | 注册事件名、字段、owner、用途 | 半年后无人知道这个事件是谁加的 |
| 变更 | 通知下游、保留旧字段过渡期 | 下游静默取到空值 |
| 下线 | 标记废弃、观察零流量后再删 | 直接删导致历史报表断裂 |
埋点体系最常见的腐化是只增不减:三年积累几百个事件,能说出用途的不到三成,采集成本照付,校验规则没人维护。
# 埋点注册表:一条记录进仓库、进评审,禁止口头新增
event: checkout_success
owner: payment-team
status: active # active | deprecated | sunset
fields:
order_id: {type: string, required: true}
amount: {type: decimal, unit: cent, required: true}
consumers: [gmv_daily, funnel_report] # 下线前逐个确认
consumers 这一栏是下线的依据:没有它,删事件只能靠猜,猜错的代价是一张看板归零。
5. 四类质量问题
| 问题 | 识别方式 | 典型成因 |
|---|---|---|
| 漏埋 | 期望量级与实际量级比 | 新分支未加埋点、SDK 未初始化 |
| 错埋 | 字段值与业务事实抽样比对 | 参数顺序错、单位错、枚举写反 |
| 重复埋 | 同一事件多来源计数对比 | 前端重试、后端重发、双端都记 |
| 版本漂移 | 分版本看指标断层 | 发版改了语义但未改版本号 |
后两类最难发现:漏埋表现为"量少",重复埋与漂移表现为"量正常但含义变了"。所以校验不能只查非空与量级,还要查分布:枚举分布、分位数、人均事件数。
-- 埋点有效率:通过校验规则的事件占比
SELECT e.event_name,
COUNT(*) FILTER (WHERE v.result = 'pass')::numeric
/ NULLIF(COUNT(*) FILTER (WHERE e.status = 'active'), 0) AS valid_rate
FROM event_registry e LEFT JOIN event_validation v USING (event_name)
WHERE e.dt = CURRENT_DATE - 1
GROUP BY e.event_name ORDER BY valid_rate;
valid_rate 长期偏低的团队,通常是"埋点加了但没人配校验规则"——问题不在上报,在校验清单空着。
6. 变更埋点的兼容策略
改口径不能一步切换,否则历史与当期不可比,趋势图全部出现台阶。
| 策略 | 做法 | 代价 |
|---|---|---|
| 新旧并行 | 同时上报两份,报表只读新的 | 采集成本翻倍,需定下线日期 |
| 老口径保留窗口 | 旧字段保留 N 个月只读 | 存储与文档负担 |
| 版本号伴随 | 事件带 schema_version |
下游必须按版本分流 |
# 双写映射:并行期内两条口径同时产出,可对比、可回滚
metric: weekly_new_user
variants:
v2: {basis: register_time, exclude_internal: true}
v3: {basis: first_login_time, exclude_internal: true}
compare_window: 14d # 差异率超阈值即阻断 v3 上线
cutover: 2026-Q1 # v2 转只读,保留历史
并行期的价值不是"稳",是给出可证伪的差异:连续两周差异率在预期内才敢切;差异率说不清,就别切。
7. 前端埋点的三个特殊困难
| 困难 | 表现 | 缓解 |
|---|---|---|
| 网络丢失 | 页面关闭时请求被取消 | 用 sendBeacon 或 keepalive 通道 |
| SDK 阻塞 | 同步加载拖慢首屏 | 异步加载 + 队列缓冲 |
| 隐私限制 | 部分字段不可采集或被浏览器拦截 | 采集前先做字段分级 |
// 卸载类事件:beacon 在页面销毁后仍会被投递,fetch 会被取消
navigator.sendBeacon('/collect', new Blob([JSON.stringify({
event: 'page_leave', ts: Date.now(), path: location.pathname,
})], { type: 'application/json' }));
隐私限制也是数据质量问题:被拦截的字段表现为"某些浏览器空值率高"。方案若没预留降级字段,指标会按浏览器分裂,而这种分裂很难被当成问题发现。
8. 口径的版本化与文档化
口径应当像 API 一样被管理:有版本、有 owner、有变更记录、有消费者清单。文档写在 wiki 里的问题是它不会随代码变,而写在配置里的口径可以被 CI 校验。
metric: weekly_active_user
version: v4
owner: growth-data
numerator: count(distinct user_id)
denominator: null
window: 7d # 滚动窗口,按 event_time 归集
dedup_key: user_id
filters: [is_bot = false, is_internal = false, event_type in (login, core_action)]
consumers: [weekly_report, exec_dashboard]
changelog:
- {version: v4, change: 排除内部账号, date: 2026-01, recompute: true}
- {version: v3, change: 加入核心行为, date: 2025-08, recompute: false}
有了 consumers 与 changelog,口径变更才是一次可通知、可追溯的事件,而不是某人默默改了一行 SQL。
9. 口径变更的通知与历史重算
| 变更类型 | 是否重算历史 | 理由 |
|---|---|---|
| 排除条件修正(如剔除测试账号) | 通常要 | 旧数据本身是错的 |
| 业务语义扩展(加入新行为) | 通常不要 | 重算会抹掉真实的行为变化 |
| 去重键调整 | 要,且需并线观察 | 新旧不可比,必须双线过渡 |
判据是**"旧数据是错的,还是旧数据对但口径不同"**:前者必须重算,后者重算等于篡改历史。重算前答三个问题——影响哪些指标、影响多少期、下游是否已消费过旧值,第三个最常被漏掉。
10. 自埋自用的陷阱与度量
最常见的腐化是激励问题:负责功能的人同时负责证明功能有效,于是口径被微调——分母去掉不活跃、时间窗 7 日改 30 日。每次调整都合理,合起来只有一个方向:让数字变好。
| 指标 | 定义要点 | 健康信号 |
|---|---|---|
| 口径文档覆盖率 | 有 owner+版本+消费者的指标占比 | 上升,优先覆盖对外指标 |
| 埋点有效率 | 通过校验规则的事件占比 | 上升,校验规则同步增长 |
| 口径变更次数 | 按季度统计,区分修正与重定义 | 稳定,且每次都有 changelog |
防自埋自用的办法不是禁止改口径,而是要求改口径必须走与改代码相同的评审:谁提、谁批、影响谁、要不要重算。把"调口径"的成本抬到与"改代码"相当,随手调整就会消失。
11. 排错:两个报表两个数时的排查顺序
| 现象 | 先查 | 常见结论 |
|---|---|---|
| 差异固定为某个百分比 | 排除条件(内部账号、机器人) | 一侧未过滤测试流量 |
| 差异随时段变化 | 时间窗与事件时间/处理时间 | 跨零点切分或延迟归集 |
| 差异随时间扩大 | 去重键 | 多端或多设备被重复计数 |
第一条几乎必中:内部账号是最常见的固定比例差异来源,也最容易被漏进对外口径。
12. 边界:什么时候不必建口径管理体系
| 场景 | 判断 | 替代做法 |
|---|---|---|
| 内部工具、无对外数字 | 错也只影响自己 | 报表旁写清口径即可 |
| 无数据驱动决策 | 数字不进入任何判断 | 不做体系,只做基本校验 |
| 指标只被一个人看 | 无跨方对齐需求 | 该人自己维护口径 |
| 一次性分析、不复用 | 无历史可比性 | 分析脚本自带注释 |
判据:**当"两个数不一致"不会导致任何决策分歧时,口径管理的收益为零。**体系价值来自多方共用一个数;只有一方时,它只是文档负担。
13. 自测
- 判断一个事件该做成前端埋点还是后端埋点,看哪一句判据?
- 只写名字的口径有什么特征,为什么它无法被证伪?
- 三方所有权里,"三不管"的典型后果是什么?
- 埋点只增不减,三年后最直接的代价是什么?
- 哪两类质量问题表现为"量正常但含义变了"?
- 口径变更要不要重算历史,判据是哪一句?