核心模块(上):事件、身份与会话,才是平台的地基
一条事件要过七道关口,每一道都在改写它。平台真正的资产不是 SDK 和图表,而是可解释的事件契约与可审计的身份图谱。
先给判断:埋点平台 80% 的长期成本,不在 SDK、存储和图表的报价单上,而在事件被定义之前、身份被合并之后。 SDK 可以换,引擎可以换,图表可以重画;事件叫什么、属性是什么类型、两个 ID 该不该合并、一次使用从哪里算起,这四件事一旦上线,就会成为所有漏斗、留存、人群和实验的隐含前提。
先做一道算术题。同一批事件,按 device_id 去重、按 user_id 去重、按付费账户去重,会得到三个不同的日活,而且每个都能自圆其说。埋点平台的大部分争议不是“采集失败”,而是三个定义被当成了一个。
本章沿一条事件的实际路径走:它怎样被发出、被命名、被合并、被切分会话、被验收、被同意约束,最后落到十条能写进研发流程的规范。
8.1 一次事件的完整旅程#
一条事件从用户手指落下到出现在看板上,要穿过七道关口,每一道都在改写它。
图 8-1 把七道关口从左到右排开,并在每道关口下标出最常见的失败。它想说明的不是“链路很长”,而是:没有任何一道关口能独立保证语义正确,语义是在传递中被逐步改写的。 采集端丢了一个属性,接入端补了一个默认值,加工端又把时区当成 UTC,最后看板上的“昨天”已经不是用户经历的“昨天”。
| 关口 | 这一关做什么 | 最典型的失败 | 该看的指标 | 谁负责 |
|---|---|---|---|---|
| 01 SDK 采集 | 生成 event_id、补上下文、读同意状态、排队重试 |
漏埋、重复、PII 误采 | 发送成功率、SDK 版本分布 | 客户端 / 前端 |
| 02 Collector 接入 | 鉴权、限流、解压、schema 初检、bot 过滤 | 返回 200 但没入库;流量峰值被打挂 | 2xx 率、p95、拒收率 | 平台 / SRE |
| 03 队列缓冲 | 削峰、解耦、保留可回放 | 热点分区、积压、保留期太短 | consumer lag、分区吞吐 | 数据平台 |
| 04 加工 | enrichment、身份合并、会话切分、去重 | 迟到数据改历史、合并不可逆 | checkpoint、状态大小、迟到 P95 | 数据工程 |
| 05 建模 | 事件表、用户表、会话表、维度与指标 | 同名不同义、指标定义打架 | 口径变更记录、plan 覆盖率 | 数据治理 |
| 06 存储 | 分区、副本、TTL、冷热分层 | part 过多、合并落后、删除不彻底 | 扫描字节、part 数、删除时长 | 数据平台 |
| 07 查询与行动 | 漏斗、留存、人群、实验、告警 | 查询慢被绕过、洞察无法行动 | 查询 p95、采用率、行动转化 | 分析 / 产品 |
治理不在这条链路的某一层,它横切每一层:同意在 SDK 生效,schema 在接入与加工校验,审计在存储与查询留痕。把治理留到“最后再加”的项目,最后都要重跑历史。
四个时间戳,别只留一个。 Snowplow 把一条事件的时间拆开:设备产生 dvce_created_tstamp、设备发出 dvce_sent_tstamp、Collector 接收 collector_tstamp、落库 load_tstamp,并明确“迟到”至少有两种——离线导致的 late sent,和批处理/管道导致的 late loaded(Snowplow 迟到数据)。只保留一个时间戳的团队,第一次补数就会开始争论“昨天的报表为什么变了”:因为“昨天”指的是设备时间、接收时间还是落库时间,从来没有被定义过。
接入层应该只做三件事:认人、验形、落盘。 认人是 write key 与域名白名单;验形是 schema 版本、必填字段、属性类型与大小上限;落盘是批量写队列,成功返回 202,失败返回可重试的 429 或 503。这里有一个廉价但致命的错误:重试时在 Collector 里重新生成 event_id,于是每一次重试都变成一条新事件,重复率随网络质量一起上升。幂等键必须由客户端生成,并贯穿所有重试。Snowplow 把链路拆成 Collect → Validate → Enrich,坏事件进 bad stream,而不是让 Collector 做业务判断(Snowplow 管道)。
客户端不保证送达。 Segment 的 analytics.js 最多缓存 100 个事件,页面关闭时用 keepalive 刷队列,而 keepalive 的载荷上限是 64 KB,队列超过这个体积就可能丢一部分(analytics.js)。所以“采集成功率”不是 SDK 的宣传指标,而是要在接入层用接收量除以发送量、按小时对账的运维指标。
判断:事件时间、接收时间、落库时间是三个不同的真相。只留一个,迟到、乱序、补数、会话重算都会变成无法解释的报表差异;留了三个却不定义哪个用于报表,只是把争论从“数据不准”升级为“口径不同”。
8.2 埋点方式与取舍#
覆盖率与语义质量成反比,这是埋点方式的第一性事实。 越自动,越容易收到一堆没有业务含义的点击;越手动,越容易漏掉没被想到的路径。选型不是选“最好的一种”,而是选一组配比。
| 方式 | 覆盖率 | 语义质量 | 前期成本 | 长期成本 | 典型风险 |
|---|---|---|---|---|---|
| 手动埋点 | 低—中 | 高 | 中 | 中,随版本漂移 | 漏埋、命名不一致、发版后失效 |
| 全埋点(autocapture) | 高 | 低—中 | 低 | 高,属性爆炸与清洗 | 把 UI 结构当语义、误采隐私 |
| 可视化圈选 | 中 | 中 | 低 | 中,页面改版即失效 | 选择器脆弱、跨端不一致 |
| 服务端埋点 | 取决于业务系统 | 高,字段可信 | 高 | 低—中 | 缺客户端上下文、覆盖不到匿名浏览 |
| 日志 / 数仓导入 | 历史全量 | 取决于源系统 | 高 | 高,schema 对齐 | 没有身份、时间语义错、重复 |
PostHog 是“高覆盖”的极端样本:全埋点覆盖交互、导航、剪贴板、热图、无效点击、异常、回放、Web Vitals 和生命周期,Web 与 React 默认开启(PostHog Autocapture)。这解释了它为什么接得快,也解释了它为什么必须配套屏蔽、遮罩与事件定义治理——自动采集把“要不要收”这个决定推迟到了事后,而事后只能靠删除。
服务端采集则是另一套逻辑。GA4 的 Measurement Protocol 明确“补充而非替代”自动采集:没有 gtag、GTM 或 Firebase 打底,服务端事件无法建立完整的 session 上下文(GA4 Measurement Protocol)。Segment 的 HTTP API 可以把服务端当成第一来源。两者经常被混为一谈,实际含义不同:一个是“补客户端看不见的事实”,一个是“服务端就是主链路”。
一个电商站的最小组合通常长这样:客户端发页面、曝光、加购、结算开始,服务端发支付成功、退款、金额与优惠券核销。客户端负责“用户走到了哪一步”,服务端负责“这笔交易到底成没成”。把金额交给客户端,等于把对账交给了一个可以被改写的环境。
8.3 事件 taxonomy 与 tracking plan#
命名规范是最没有技术含量、也最容易被跳过的一节,但它决定了半年后一个新同学能不能不问人就把查询写对。 tracking plan 不是事件清单,它至少要回答四件事:谁在什么条件下发、带哪些属性、属性是什么类型、坏了谁负责。
Segment 的规范很具体:事件名用 Title Case,属性名用 snake_case,事件名采用 Object + Action(如 Order Completed);不要动态生成事件名,不要把值塞进事件名,也不要生成 feature_1、feature_2 这类属性键(Segment 最佳实践)。GA4 走另一套:事件名只能字母开头、只含字母数字与下划线、最长 40 字符;每个事件最多 25 个参数,参数值最长 100 字符;用户属性最多 25 个,推荐事件用 sign_up、add_to_cart 这类 snake_case(GA4 命名规则、GA4 采集上限)。
两套规范都合理,混用会出事。同一家公司 Web 用 GA4、App 用 Segment,于是 Add To Cart 和 add_to_cart 同时存在;BI 层要么大小写折叠,把两个语义不同的事件合成一个,要么接受双份。跨国团队最常见的返工不是缺事件,而是同一个事件有两个名字。
| 对象 | 推荐写法 | 常见反例 | 代价 |
|---|---|---|---|
| 事件名 | checkout_completed 或 Checkout Completed,全站统一 |
click_btn_1、checkout_v2_final |
事件名变成版本号,半年后没人敢删 |
| 属性名 | payment_method、order_id |
payType、paymentMethod、pay_type |
同一含义三种拼写,漏斗要写三段 OR |
| 枚举值 | apple_pay、wechat,词表固定 |
ApplePay、applepay、AP |
分组时漏掉一批订单,且不易发现 |
| 动态键 | coupon_code 一个属性 |
coupon_1、coupon_2、coupon_3 |
属性爆炸,无法聚合,只能逐个写 SQL |
一份可执行的 schema 大致长这样:
event: checkout_completed # object_action,snake_case
version: 2
owner: growth-checkout
status: active # active | deprecated | experimental
properties:
order_id: {type: string, required: true, format: uuid}
payment_method: {type: enum, required: true, values: [card, apple_pay, wechat]}
item_count: {type: integer, required: true, min: 1}
coupon_code: {type: string, required: false, maxLength: 32}
这段 YAML 不难写,难的是改动管理。Segment 用 context.protocols.event_version 让新旧 App 版本并行校验(Segment 版本);Snowplow 把 schema 变更分成 major、minor、patch,而且同一个“必填改可选”,在 Redshift 是 breaking,在 Snowflake、BigQuery、Databricks 却不是(Snowplow 版本规则)。同一个动作,在不同数仓上的风险等级不同,这件事必须在评审时说出来。
工具层已经把 plan 产品化了,只是路径不同。Avo 把 plan 的分支、审批、codegen 与 Inspector 串起来:Inspector 采集线上事件形状与 plan 对比,按 issue 分类;免费版 2 个 editor、10 万 Inspector events,Team 250 美元/月(年付),加 editor 50 美元/月(Avo 文档、Avo 定价)。Mixpanel 的 Data Standards 把命名、描述、owner 做成可评分的合规状态,但只在 Enterprise 提供;Amplitude 的 tracking plan 支持必填、类型、正则和属性组(Mixpanel Data Standards、Amplitude Tracking Plan)。它们的共同前提是:有人愿意为“事件定义”签字。没有这个前提,工具只是把 Excel 换成了更贵的 Excel。
判断:tracking plan 不是文档,是跨团队契约。它必须和代码一起进版本库、和发版一起评审、和报表口径一起归档;否则它只会变成一张过期的表,而且是所有人都引用、没人负责的那张。
8.4 属性、版本与数据契约#
属性设计的第一原则不是“多收一点以后有用”,而是“每个属性都能回答一个具体问题”。 收进来的每个字段都要付三次钱:传输、存储,以及未来每一个要理解它的人。
几个不写进规范就一定会出错的点:金额用整数分或 decimal,不用浮点;时间统一 UTC,另存时区字段;枚举用受控词表,未知值进 unknown 而不是自由文本;布尔不要出现 Y、N、1、true 四种写法;PII 在 plan 里显式标注,不藏在自由文本里。属性还会自我膨胀:一张事件表打平 200 个属性,扫描字节会翻倍,长尾属性应该进 JSON 或 Map,按需取。这不是存储问题,是查询成本问题——每一次全表扫描都在为“以后可能有用”付费。
更正式的做法是数据契约。Open Data Contract Standard 把 schema、质量规则、SLA、owner、服务级别写进 YAML,作为 plan 与管道之间的中间层(ODCS)。它的价值不在 YAML 本身,而在它迫使数据生产方与消费方在同一张纸上写下:什么算坏、坏了谁负责、多久修好。
| 变更 | 版本位 | 历史查询会怎样 | 处理方式 |
|---|---|---|---|
| 新增可选属性 | minor | 不受影响 | 直接发,plan 同步,报表按需接 |
| 必填改可选 | minor,部分引擎视为 breaking | 旧数据满足,新数据可能为空 | 下游先加空值分支,再评估引擎兼容 |
| 类型 string 改 number | major | 隐式转换或报错,聚合静默出错 | 新属性名双写,旧字段保留一个版本周期 |
语义变更(如 plan_type 值域) |
major | WHERE 静默返回 0 行 |
换新事件名或新枚举,附映射表 |
版本策略可以压成三句话:新增可选属性升 minor,破坏性变更升 major,语义变更换新名字而不是原地改。事件用 active、deprecated、experimental 三态管理;废弃先双写、再切读、最后删除,不做原地改名。
一个具体反例:把 plan_type 的取值从 pro 改成 professional,在 code review 里像一次文案修改,在历史查询里是一次静默事故——WHERE plan_type = 'pro' 从此返回 0 行,看板不会报错,只会变成一条水平线。等有人发现,通常已经过去一个季度。
一次变更是否破坏性,标准不是“代码会不会报错”,而是“旧查询会不会静默算错”。 前者在发版时暴露,后者在季度复盘时才发现,代价差一个数量级。
8.5 身份解析#
身份解析(identity resolution)的目标不是“把同一个人合并成一条记录”,而是“在需要的时候能合并,在证据不足时敢不合并”。 前者是算法,后者是产品。
身份系统通常要处理四类 ID:匿名 ID(anonymous_id 或 device_id)、登录 ID(user_id)、账号或组织 ID(account_id、group_id)、外部 ID(email、phone、CRM ID)。大多数事故发生在第三类和第四类:把 account 当 user,或者把 email 当唯一键。
各家给出的答案差别很大,而且差别不在“能不能合并”,在“合并失败时怎么办”。PostHog 最保守:匿名事件不创建 person profile,调用 identify 才创建并把之前的匿名事件并进来;如果两个档案都已经 identify 过,系统直接拒绝合并并记录 Refused to merge an already identified user,因为它无法判断哪个是真身(PostHog Identify)。Amplitude 同时使用 device ID、user ID 与 Amplitude ID,优先 user ID,但标准项目无法把两个已确立 user_id 的历史活动合并,只能在未来把新活动归到正确 ID(Amplitude 身份)。这句话对迁移项目很关键:合并错了,改不回去。
Mixpanel 用 $device_id 与 $user_id 首次同现形成 identity cluster,Simplified ID Merge 把 canonical distinct_id 固定为 $user_id;Original API 每个 cluster 最多 500 个 distinct ID,Simplified 取消了该上限,但迁移必须新建项目(Mixpanel 身份合并、迁移说明)。Snowplow 是确定性图谱:合并时旧 ID 成为规范 ID 并发出 merge event;如果合并会违反唯一标识约束——例如同一设备出现两个不同 user_id——系统不猜,直接为无法归属的标识新建匿名 ID(Snowplow Merges、唯一标识)。GA4 有三个身份空间(User-ID、device ID、建模),Reporting Identity 可选 Blended、Observed、Device-based,政策明确禁止在未获同意时把已登录与未登录会话 stitch 起来(GA4 Reporting Identity、GA4 政策)。
身份还会直接进账单。Segment 的 MTU 定义为计费周期内唯一 userId 数,加上未关联 userId 的唯一 anonymousId 数;同一用户跨 source 只算一次,但未登录的 Web 与 App 会各算一个匿名 MTU(Segment MTU)。所以“尽早 identify”既是分析问题,也是成本问题——这也是为什么很多团队在登录墙前就开始要手机号,而这个决定后来往往被隐私团队推翻。
边界案例比主流程更能说明问题:
| 场景 | 直觉做法 | 后果 | 更稳的做法 | 判定规则 |
|---|---|---|---|---|
| 共享设备,B 用户登录 | 把 A 的 anonymous_id 并到 B |
A、B 的历史混成一个人 | 登出 reset(),登录新建 cluster |
设备所有权未验证,不合并 |
| 先匿名、后登录 | 登录时重写历史事件的 user_id |
不可回滚,审计断链 | 保留匿名 ID,追加 alias/merge 事件 | 只追加映射,不改写事实 |
| 同一人两个已登录账号 | 强行 merge | 删除请求无法定位全部数据 | 建 person → account 层级,不合并 user | 账号边界由业务定义,不由算法猜 |
| 邮箱大小写或变更 | 把 email 当唯一键 | 重复档案或误并 | 内部 immutable ID,email 只作可更新属性 | 可变属性不进主键 |
| 跨域投放页 | 只传 user_id |
匿名段丢失、会话断裂 | 传 device_id 加 session_id |
会话连续性需要两个 ID |
| B2B 多成员 | 把 account 当 user | 席位、权限、行为全错 | Group/account 独立建模 | 计费单元不等于行为主体 |
| 家庭共享订阅 | 当 user 合并 | 多个人的行为混成一个人 | 单独 household_id,只用于计费与内容权限 |
不参与行为身份合并 |
自研系统至少要有一个冲突队列,把“无法判定”的合并请求挂起等人处理,而不是静默取最新值。Snowplow 的唯一标识冲突和 PostHog 的拒绝合并,本质上是把这个队列产品化了;区别只是 Snowplow 会为无法归属的标识新建匿名 ID,而 PostHog 选择保留两个档案。两种都不完美,但都可解释。
判断:身份系统的成熟度,不看它能合并多少,而看它敢拒绝多少。没有“不合并”能力的系统,最终一定会制造一批无法解释的重复用户,而且这些重复会同时出现在报表、人群和账单上。
8.6 会话与归因上下文#
会话是最像事实、其实最像观点的对象。它不在事件里,它在事件之间。
Segment 的六个原语里根本没有 session(Segment Spec);Mixpanel 明确说会话在查询时计算,改定义会回溯历史(Mixpanel Sessions);GA4 默认 30 分钟不活动结束,没有最大时长,可以用 extend_session 延长(GA4 Sessions);Amplitude 的移动端在进入后台 5 分钟后结束会话(Amplitude Sessions);PostHog 默认超时也是 30 分钟,但最大时长 24 小时(PostHog Sessions);Snowplow 把会话做成 client_session 实体,移动端前后台可以分别配置(Snowplow 会话)。
| 产品 | 默认超时 | 最大时长 | 前后台与重置 | 可重算 |
|---|---|---|---|---|
| GA4 | 30 分钟不活动 | 无上限 | 进后台开始计时,extend_session 可延长 |
否,仅能延长 |
| Amplitude | Web 30 分钟;移动后台 5 分钟 | 文档未给固定上限 | 前后台分别计时 | 有限 |
| Mixpanel | 30 分钟,可配置 | 未给固定上限 | 可改为事件型或 session_id 属性型 |
是,改定义回溯历史 |
| PostHog | 30 分钟不活动 | 24 小时 | 同一浏览器/设备,可跨 tab | 部分 |
| Snowplow | Web 30 分钟;移动前后台各 30 分钟 | 无上限 | 移动可分别配置 | 是,建模层可重算 |
| Segment | 无原生会话 | — | — | 下游自算 |
这张表的重点不是记住数字,而是看最后一列:能不能重算。能重算,会话定义就可以迭代;不能重算,一次配置失误会永久留在历史里。Mixpanel 把重算做成默认能力,代价是“改定义会改历史”,于是任何口径调整都要提前通知报表使用方——这不是技术问题,是协作问题。
跨域是会话断裂的高发场景。Amplitude 的要求很具体:跨域时要把 device ID 和 session ID 一起通过 URL 传过去,只传 device ID 会保留匿名身份,但会丢掉会话连续性(Amplitude 跨域)。
归因窗口在分析层配置,不在采集层。GA4 的 Attribution Settings 默认获客转化 30 天(可选 7 天或 30 天),其他转化 90 天(可选 30、60、90 天),改模型会同时影响历史与未来报表(GA4 归因设置);session_start 携带 gclid、UTM、referrer,归因模型是非直接上次点击。这意味着“渠道效果”不是一个采集事实,而是一个可配置的分析结论。把它当成事实写进周报,迟早会和市场部的口径打架。
深链是另一个被低估的上下文来源。Firebase Dynamic Links 已在 2025-08-25 停服,所有 page.link 域名与自定义域名链接停止工作;依赖它传递 campaign、邀请码、商品 ID 的团队必须迁移(Firebase 公告)。迁移的不只是链接,还有“用户从哪来”这条上下文——很多团队在迁移后才发现,新用户的渠道归因突然全变成了 direct。
判断:会话是产品语义,不是技术常量。把 30 分钟写死在 SQL 里,等于把一次产品决策固化成了不可审计的假设;正确做法是把它做成可配置、可版本化、可重算的语义层,并在迁移前准备一张口径对照表。
8.7 数据质量与埋点验收#
数据质量不是“校验通过率”,而是“违规事件有没有出路”。 一条坏事件被挡住,和一条坏事件被悄悄丢掉,在报表上看起来一样,在事故复盘时完全不同。
| 出口 | 代表做法 | 数据去哪 | 能不能捞回 | 适用 |
|---|---|---|---|---|
| 放行并标记 | Amplitude 标记为 unexpected | 进主表,带标记 | 能,查询时过滤 | 探索期、低风险属性 |
| 拒绝写入 | Amplitude 拒绝事件或属性 | 不进表 | 不能,需重发 | 明确不允许的字段 |
| 阻断并丢弃 | Segment Schema Controls | 丢弃 | 默认不能,除非转发到隔离 Source | 合规红线、生产环境 |
| 隔离待修 | Snowplow failed events | 单独 warehouse 表 | 能,修好后用 SQL 合并 | 长期基础设施 |
| 隐藏待审批 | Mixpanel Event Approval | 进库但不可见 | 能,审批后可见 | 新事件治理 |
细节决定这套出口是“治理”还是“事故”。Segment 的阻断只在 cloud-mode destinations 生效,未被转发到隔离 Source 的 blocked events 会被永久丢弃(Schema Controls)。Amplitude 把违规拆成三类:未规划的事件类型、未规划的事件或用户属性、未规划的属性值;每一类都可以选“标记并摄入”或“拒绝”,拒绝事件时不存事件及属性,拒绝属性时保留事件但不存属性,且 Validation Errors 只展示最近 24 小时(Amplitude Schema)。Mixpanel 的 Event Approval 默认隐藏新事件,管理员审批后才可见;Data Volume Monitoring 用最多 24 个月历史判断预期范围,默认看前一天,可改成两天以容忍摄入延迟,新 Enterprise 项目从 2025-12 起默认开启(Event Approval、Volume Monitoring)。Snowplow 的 failed events 不写入 atomic 表,失败可能发生在收集、校验、富化、加载四段;最常见的是埋点代码与 schema 不一致,或 schema 没发布到生产 registry,修复后可以合并回去,但该功能不回溯(Failed Events)。
重复与乱序要靠幂等键和重算窗口。 PostHog 的去重键是 uuid、事件名、时间戳与 distinct_id 的组合,重复事件在 ClickHouse 后台 merge 时最终去重,所以重复可能短暂存在;修改事件时 timestamp 必须保持一致,否则不会被去重(PostHog Events)。Snowplow 用 snowplow__days_late_allowed 控制“设备产生到发出”的最大延迟,用 snowplow__lookback_window_hours 给“落库延迟”留缓冲,超过窗口的事件不会被处理。
Bot 与采样是另外两类噪声,而且往往不可见。GA4 自动排除已知机器人与爬虫,依据是 Google 研究与 IAB 的 International Spiders and Bots List,不能关闭,也看不到排除了多少(GA4 Bot 排除);Data Quality 指示器会标注 Unsampled、Thresholding applied、Heavily sampled(例如“基于 8.88% 数据”),阈值由系统决定、不能调整(数据质量、阈值说明)。对采购的含义是:你看到的不是全量数,而且平台不打算让你知道差多少。
埋点验收应该独立于生产管道。下面这张清单可以直接贴进发版流程:
| 检查项 | 通过标准 | 证据 | 谁签字 |
|---|---|---|---|
| 事件在 plan 中 | 名称、版本、owner、状态齐全 | plan diff | 数据产品 |
| 必填与类型 | 必填无空、类型匹配、枚举在词表内 | 校验报表 | 数据工程 |
| 身份字段 | 带 anonymous_id,登录后带 user_id |
抽样 100 条 | 客户端 |
| 同意状态 | 带 purpose、CMP 版本、时间戳 | 请求或属性 | 隐私 |
| 时间戳 | 设备时间与接收时间差在窗口内 | 迟到 P95 | 数据平台 |
| 重复与丢失 | 重复率低于 0.1%,发送与接收差异可解释 | 对账日报 | 数据平台 |
| 出口与保留 | 目的地、保留期、删除路径明确 | 数据流图 | 架构 |
| 回滚 | 关闭开关后不产生脏数据 | 演练记录 | 发布负责人 |
八项里任何一项没有 owner,都不算“埋点完成”。验收也不该只在上线前做一次:Avo Inspector 这类工具的价值,是把“线上事件形状”持续对比 plan,而不是在发版评审会上对着文档点头。
运行期至少要有六个指标并配 owner:采集成功率(2xx 除以总请求,目标不低于 99.9%)、采集 p95(低于 500 ms)、端到端新鲜度、重复率(低于 0.1%)、schema 违规率(低于 0.5%)、bot 与异常流量占比。这些阈值来自本报告第 10 章的架构建议,需要按业务校准,但“有没有指标”比“指标定在哪”更重要。
判断:最危险的不是脏数据,是静默丢数据。脏数据至少还能在报表上看见异常;静默丢失只会让所有数字一起变小,而且每个人都以为那是业务下滑。
8.8 隐私与同意#
同意不是 banner 上的一个布尔值,而是一组带版本、带目的、可审计的事件属性。
GDPR 的三条底线直接落在埋点上:第 5(1)(c) 条要求数据最小化,只收集与目的相关且必要的数据;第 6 条要求至少一个合法性基础;第 7 条要求同意可证明、可撤回,且撤回与给出一样容易(GDPR 原文)。EDPB 05/2020 指南进一步要求同意是自由给出、具体、知情、明确的意思表示(EDPB 指南)。
广告技术有自己的一套语言。IAB Europe 的 TCF 2.2 于 2023-05-16 上线,实施截止从 2023-09-30 延到 2023-11-20,核心变化之一是取消 purposes 3–6 的 legitimate interest 基础,只能选 consent(TCF 2.2)。Google 的 Consent Mode v2 在 2023-11 增加 ad_user_data 与 ad_personalization,与 ad_storage、analytics_storage 一起构成四种同意状态;EEA 流量若没有把同意信号传给 Google,广告个性化与再营销能力会受限(Consent Mode、EEA 说明)。
产品侧的落地差异很大。Segment 要求把 consent object 加到所有事件上,再按类别决定路由到哪些目的地;Consent Management 只在 Business 套餐提供,且官方不建议用它管理通信订阅偏好(Segment Consent)。PostHog 提供 opt-out/opt-in、IP 采集开关、autocapture 屏蔽、敏感信息遮罩、cookieless 模式,但这些是采集端控制,不等同于 CMP(PostHog 数据采集控制)。GA4 对 EU、CH、UK 用户不记录、不存储 IP,只用 IP 做粗粒度地理派生后立即丢弃;User-ID 必须是不可反推的个人标识(GA4 EU 数据)。
保留期是隐私设计的一部分,不是运维参数。GA4 用户级数据只能选 2 或 14 个月,其他事件数据标准版为 2 或 14 个月,360 才可选 26、38、50 个月(GA4 保留策略);PostHog 付费版事件保留 7 年、免费版 1 年(PostHog 定价)。同一家公司如果同时用两者,“一年后还能不能查到这个人”会有两个答案。
中国是另一套规则。《个人信息保护法》第 6 条要求目的明确、合理,与目的直接相关,采取对个人权益影响最小的方式,限于实现目的的最小范围;第 13 条列出同意、合同必需、法定义务等合法性基础;第 38 条要求跨境提供满足安全评估、标准合同或认证等条件(PIPL 原文)。2024-03-22 发布的《促进和规范数据跨境流动规定》给出更具体的门槛:非 CIIO 自当年 1 月 1 日起累计向境外提供不满 10 万人个人信息(不含敏感)可免申报;10 万至 100 万人需标准合同或认证;100 万人以上,或 1 万人以上敏感个人信息,需安全评估(CAC 规定)。
同意的技术难点不在收集,而在传播。 它必须在 SDK 发请求之前生效,并沿身份图谱传播到下游每一个目的地;否则“用户撤回了同意”只是一句 UI 文案,数据该在哪还在哪,而且没有任何一条日志能证明它被删过。
8.9 边缘采集与中国特殊环境#
采集端点已经从一个运维细节,变成了产品能力。 请求发到哪个域名、走谁的证书、在哪个区域落地,直接决定事件量。
广告拦截是第一方采集最直接的动机。PostHog 官方称反向代理通常能把事件采集量提高 10–30%,具体取决于用户群;它建议使用中性子域,不要带 analytics、tracking、telemetry、posthog、ph 等词(PostHog 反向代理)。这是厂商自测口径,不能当成普遍规律,但方向成立:请求域名是拦截器最直接的信号。Segment 的 Custom Domain 是 Business 套餐能力,提供 HTTPS 第一方子域,支持 DNS 委派或 CNAME,但明确不能用于 device-mode destinations,因为那些 SDK 必须直接请求目的地(Segment Custom Domain)。
Google 在 2025-05-02 开始推送 Google tag gateway for advertisers,让 Google 标签走广告主自己的 CDN、负载均衡或 Web 服务器;官方称已配置广告主信号提升 11%(厂商口径),并计划默认启用 confidential computing(Google 公告)。Cloudflare 在 2025-05-08 宣布集成,把第一方标签配置压缩到几分钟(Cloudflare)。
浏览器侧的约束没有消失,只是换了形状。Safari 从 2020-03 起默认阻止第三方 cookie(WebKit ITP);Chrome 在 2025-04-22 决定维持第三方 cookie,不推出新的独立提示,但继续在 Incognito 中默认阻止,并推进 IP Protection(Privacy Sandbox)。CNAME cloaking 随之出现:把第三方追踪域名伪装成站点子域,绕过基于域名的拦截;WebKit 曾以限制 CNAME 伪装响应中持久 cookie 的有效期作为反制(WebKit Bug 215201)。这意味着第一方域名不是一次性配置,而是需要持续监控 DNS、证书与 cookie 行为的工程。
中国把这个问题变得更硬。微信小程序必须先配置通讯域名,只能向指定域名发请求,域名必须 ICP 备案,不能配置父域名,端口必须精确匹配,api.weixin.qq.com 不能被配置为服务器域名(微信网络能力)。这一条直接排除了“用一个未备案的 SaaS 采集域名”的方案。We分析要求先定义事件再上报,wx.reportEvent(eventName, data) 自基础库 2.14.1 起可用(微信事件管理)。HarmonyOS Analytics Kit 则明确要求 SDK 初始化必须在用户同意隐私协议之后,AAID 在重装、恢复出厂、清除应用数据或调用 clearCachedData 时重置(HarmonyOS 接入)。
信创环境(国产操作系统与浏览器)的采集兼容性缺少公开的逐项清单,本次未能核实,只能标“待核实”(证据等级 C)。可行做法是把国产 OS、浏览器、WebView 内核纳入采集矩阵,逐项验证 Cookie、localStorage、CORS、TLS、证书链、代理与请求体大小,并保留测试记录——不要用“兼容国产化”一句话替代 POC。
边缘采集的失败不会报错,只会“少一点”。 域名、证书、备案、反代、同意门控任何一环出问题,损失都直接体现在事件量上;所以它必须有 owner、有对账、有告警,而不是交给某个人的一次配置。
8.10 十条落地规范#
前面九节讲的是“为什么会错”,这一节讲“怎么让它不容易错”。十条规范按执行顺序排列,每条都能直接写进研发流程。
- 先定指标与事件,再写 SDK。 tracking plan 先过产品、数据、工程三方评审,每个事件必须说明它服务哪个指标;没有指标的事件不进 plan。这一条通常能砍掉相当一部分“以后可能有用”的采集。
- 固定命名与类型。 事件用
object_action,属性用 snake_case;禁止动态事件名与动态属性键;枚举列全,未知值走unknown。命名规范要写进 lint,不要只写进 wiki。 - 每个事件有 owner、描述、版本、状态。 用
active、deprecated、experimental三态;废弃先双写、再切读、最后删除,不原地改名。没有 owner 的事件,半年后一定没人敢动。 - 客户端只发事实,不发派生指标。 金额、订单状态、权限、退款以服务端为准;客户端补页面、设备、实验分组这类上下文。客户端算出来的转化率只能用于排查,不能进报表。
- 匿名 ID 由采集端生成并持久化,登录只做关联。 登录不重写历史
user_id;登出必须reset();共享设备新建 cluster。合并只能追加映射,不能改写事实。 - 会话定义写成配置并版本化。 超时、前后台、跨天、最大时长、campaign 捕获点全部显式;会话表可重算。不要把 30 分钟写死在查询里。
- 采集端做轻校验,服务端做权威校验。 必填、类型、枚举、长度在 SDK 与 Collector 先挡一层;完整 schema 在服务端执行;失败进隔离区,不静默丢。
- 幂等三件套:
event_id、事件时间、接收时间。 重试与补数都带同一个去重键;迟到窗口、重算窗口、回放流程写进 runbook,而不是留在某个人的记忆里。 - 同意状态作为事件属性。 记录 purpose、category、CMP 版本、时间戳;在发请求前 gate;删除请求要能沿身份图谱定位到所有关联 ID。
- 把指标、阈值和 owner 一起写进 SLO。 采集成功率、schema 违规率、身份合并率与孤儿率、重复率、迟到 P95、bot 占比;每个指标有 owner 和告警阈值。没有 owner 的指标,第一次告警就会被静音。
如果只有 90 天,顺序应该是:第 1 至 30 天写 plan 与身份规则,第 31 至 60 天做对账与隔离区,第 61 至 90 天再谈实时与自动化。反过来做——先上实时、先买人群运营——通常会在第三个月发现所有数字都要重算,而重算的前提,恰好是前 60 天没做的那两件事。
判断:如果只能保留一项能力,保留“可解释的事件契约 + 可审计的身份图谱”。SDK、存储、查询、图表都可以换;这两样换了,等于把公司过去三年的所有结论重做一遍。