Co-Authored-By: Claude Code <noreply@anthropic.com>
12 KiB
采集底座修复(阶段A)— 设计文档
日期:2026-09-18 分支:m1 状态:设计已获用户批准(含两轮自审修订) 依据:gpt/14-原始上报数据分析与采集优化实施方案.md(V1.3),范围 = 阶段A(O00-O03) 前置探索:2026-09-17 代码现状核验,文档全部"代码事实"确认属实
背景与目标
M1 采集底座已在生产运行(hesucar.com,日访客 400+)。2026-09-16 真实样本分析(gpt/14)暴露采集底座的结构性缺陷,其中 P0 两项:
- D02 事件归属漂移:SDK 在 flush 时才调 getSessionId(transport.ts:44-45),事件产生与会话判定最长错位 15s;样本中 5 个孤立停留会话(8 条 page_stay 无同会话 page_view)与此吻合。事件无 page_id,归属真相活在批次信封层。
- D08 URL 敏感值清洗缺口:sanitize_url 只按参数名黑名单删(email/phone/token 等名),白名单参数名下藏敏感值不会被过滤;sanitize_payload 只按 JSON 键名过滤。
阶段A 目标(文档 §10 阶段A 出口):归属不漂移、停留区间准确、清洗与用途状态生效、版本可诊断。旧 SDK(0.1.0 缓存)持续兼容,旧数据不回填。
范围
做:O00 复现台账 + O01 契约/版本 + O02 归属冻结/停留/时间质量 + O03 清洗强化。SDK 0.1.0 → 0.2.0,服务端 schema v2(兼容 v1)。
不做(后续阶段):purpose/epoch/grant 授权状态机(V1.1 治理 → M2/M3,本阶段仅预留字段位);业务事件 O04/O05(阶段B = M2 核心);sdk_diag 低频诊断事件、performance_summary、error_summary(O07 后续);admin 前端展示新字段(阶段B);站点级清洗参数配置(阶段B);历史数据 page_id 回填(文档 4.1.7 禁止);Event 表保留期限清理(现状量级不需要)。
核心架构决策
事件级归属快照(用户选定方案 A):事件入队时深快照 {site_id, visitor_id, session_id, page_id, seq} 写入事件对象;flush 用快照构造信封,不再读当前身份;混合归属队列按四元组分组拆子批。服务端按事件级归属校验落库,批级信封仅作 v1 兼容。purpose/epoch/grant_ref 预留 null 字段位,将来直接挂进同一快照。
1. SDK(0.1.0 → 0.2.0)
1.1 PageContext(新模块 sdk/src/page.ts)
- 创建:SDK init 时;SPA 路由切换时(见 1.3)。字段:
page_id(uuid)、session_id(入队时快照、flush 不再重读;会话恢复时按规则换新,见下)、visitor_id、started_at、页内seq计数器。 - 会话恢复:空闲 >30min(IDLE 沿用现值)后同页再活动 → 生成新 session_id + 发
session_resume事件(属性:prev_session_id、reason="idle_timeout"),不伪造 page_view。三个检查点:用户交互时、visibilitychange→visible时、flush 前;hidden 期间不算活动。 - bfcache 恢复(OT02):
pageshow persisted=true时内存上下文仍在 → 沿用原 page_id 复活(seq 继续),不产生新 PV;若超 30min 则补 session_resume。不引入独立 document_id。 - 每页面段关闭(路由切换/pagehide)时结算尾部停留。
1.2 事件结构升级(events.ts)
事件对象新增公共字段:site_id/visitor_id/session_id/page_id/seq + 预留 purpose/epoch/grant_ref(null)。
page_view:现有字段不变 + 公共字段;nav 信号保持 init 冻结。page_stay:独立区间语义——每条代表一个不重叠活动区间:active_from/active_to(单调时钟 epoch ms)、active_ms(区间时长)、page_id、flush_reason(periodic/hidden/pagehide/route_change)、measurement_version:"2"。详见 1.4。session_resume(新):prev_session_id、reason。sdk_loaded:sdk_version(不再硬编码)、build_id、schema_version、config_version(构建时常量,当前固定 "1",随配置结构演进升级)。
1.3 SPA 路由切换
hook history.pushState/replaceState + popstate。仅当规范化 path(去 query、去 hash)变化时开新页面段(结算旧段 → 新 page_id → 新 page_view);replaceState、hash 变化、query 变化不开段(防"地址清洗触发重复PV",文档 4.1.5)。hash 路由 SPA 站点本阶段不适配(边界,阶段B)。
1.4 停留重做(stay.ts)
performance.now()单调时钟计算区间;Date.now()仅作展示时间(occurred_at)。- 有效活动定义沿用冻结策略(可见 + 最近 30s 内有交互)+
measurement_version标记。 - 周期结算:每 15s(可配置
stayIntervalMs)将当前连续活动区间切一刀发出(flush_reason=periodic);60s 连续活动 → 4 条相邻区间,服务端并集拼回。 visibilitychange→hidden与pagehide共享结算状态(hidden 已结算则 pagehide 不重发);hidden 时刻强制结束当前区间;切回 visible 后活动恢复时开新区间(起点=活动恢复时刻)。- 区间与 event_id 绑定:入队后重试不换 ID(幂等)。
- beacon 成功入队 ≠ 服务端确认:沿用 unknown 语义(见 1.6)。
1.5 传输重构(transport.ts)
- flush 从队首事件快照取归属构造信封;队列存在多组
(visitor, session, page)时拆子批按序发送,子批独立重试(重试重放原 body 字符串,event_id 不变)。 - 乱序到达是正常现象:服务端不拒,仅标记(见 2.3)。
- 批级信封新增:
schema_version:2、sdk_version、build_id(sent_at现有 transport.ts:46 已发送,不重复开发)。 - beacon 字节预算:pagehide 发送前检查 payload 字节,超 60KB(Chrome sendBeacon 64KB 限制留余量)→ 降级 fetch keepalive,仍失败则丢最旧事件并计 dropped。
- 响应处理:解析服务端逐项结果(accepted/duplicated/rejected 明细),内存累计计数。
1.6 版本注入
esbuild --define:__SDK_VERSION__(package.json 0.2.0)、__BUILD_ID__(git short hash + 构建时间戳)。每次功能变化产生可区分构建标识(D05)。
2. 服务端(schema v2,兼容 v1)
2.1 契约(constants.py / validation.py)
- 批级
schema_version:2 = 新路径;缺省 = v1(旧 SDK),照旧处理。 - 事件白名单 +
session_resume;属性白名单扩展对应新字段。 - 事件级校验:site_id 匹配批 site、四元组完整、page_id uuid 格式、seq 软校验(乱序标记不拒绝)。
- event_id 跨批幂等:已确认存在
uq_site_event (site, event_id)唯一约束(models.py:105),重试重放双倍入库风险不成立,无需额外迁移。
2.2 模型迁移(models.py + migration)
- Event 新增:
page_id(char, null)、active_from_ms/active_to_ms(bigint, null)、flush_reason(char)、time_quality(char: ok/late/skew_backwards)、sdk_version(char)、schema_version(int)。 - VisitSession 新增:
resume_of(FK self, null)。 - RawBatch 新增:
request_id(char)。 - 索引:
page_id单列;(site,event_id)唯一约束(见 2.1)。 - 加列为 instant DDL,万级表无锁风险。
2.3 入库(ingest.py)
- v2 事件级归属:visitor/session 按事件级字段 get_or_create;批级信封仅作 v1 兼容。
session_resume:创建新 VisitSession(resume_of 指向 prev);prev_session_id 无效/不存在 → 软处理(resume_of=null + 计数告警),不拒事件。- v1 事件:沿用现有路径(含 30min sid#N 兜底);v2 的会话切换由 SDK 显式 resume 驱动,服务端兜底不介入(避免双切)。
- page_stay 聚合语义:VisitSession.stay_ms 改为重算式——受影响会话每次入库后从 Event 表按
(page_id)分组做区间并集后求和回写(当前量级重算成本低;uq_site_event 存储层去重保证重算不重复)。不按 SUM(active_ms) 增量累加(15s 增量会重复计)。v1/v2 混合解释:v1 事件(无 page_id,含灰度期与回滚期)沿用现有 active_ms 加法,v2 事件走区间并集,同会话内两种来源分别处理后相加——admin 数值只有一种解释。受影响聚合点核查清单:VisitSession.stay_ms(ingest.py F 增量处)、趋势/总览实时聚合(dashboard.py avg_stay_ms)、访客详情(visitors.py total_stay_ms/会话停留)——全部适配,保证 admin 现有页面数值正确(阶段A 不改 admin 界面)。 - time_quality:按 received−occurred 差值分类(阈值沿用现有 late 判定 + 新增负值 → skew_backwards)。
- 响应契约:
{accepted, duplicated, rejected:[{index, event_id, reason}], request_id}——rejected 明细回传;HTTP 200 不再隐含全量成功。
2.4 清洗强化(D08,validation.py + collect.py)
URL 清洗三轨制(替代现行"仅参数名黑名单"):
- 参数名黑名单(phone/mobile/tel/email/name/user/token/key/password/verif 等)→ 删;
- 归因白名单(utm_*、gclid、gbraid、wbraid、fbclid、gad_source、gad_campaignid、msclkid 等)→ 提取保留(供归因);
- 其余参数按值判别:含 @、超 32 位随机串、超长值 → 删;纯数字 ≤10 位、短 slug → 保留(未知业务参数如 ?id=123 自动保住,WordPress query 路由不受影响)。slug 安全线定 32(总长 ≤32 且 [A-Za-z0-9._-],32+ 连续字母数字视为 token 删——实现计划已用向量钉死此口径,以计划为准)。
另:URL userinfo 剥离;fragment 全删(hash 路由站点阶段B 前不适配,边界注明);title 中敏感内容按站点模板规则本阶段不做(无规则来源,边界注明)。
清洗时机:SDK 入队前 + 服务端快照前双保险;清洗前内容不写入 RawBatch/日志/导出(文档 8.3)。
双实现一致性:TS 与 Python 两份清洗逻辑,共享单源测试向量 server/tracker/tests/fixtures/sanitize_vectors.json(一组含敏感值的 URL 输入/期望输出);SDK vitest 以相对路径读同一文件,两侧断言一致。规则带版本常量(同步升降)。
3. 部署与灰度
- 服务端先上线(
docker compose up -d --build web自动 migrate)→ 线上 v1 上报回归验证(现有 142 测试 + 生产探活)。 - SDK 0.2.0 构建替换 sdk/dist(nginx 挂载 /opt/cms-cj/sdk/dist)→ 新访客逐步加载,旧缓存自然过期。
- 观察指标:孤立 page_stay 会话占比(D02 复现指标,应降为 0)、事件量增幅(15s 增量结算预计 +3~4 倍 page_stay,MySQL 万级/日无压力)、拒绝率、RawBatch 状态分布。
- 回滚安全:SDK 回滚至 0.1.0 无需回滚服务端(ingest 聚合语义与 SDK 版本解耦,v1 路径完好);Event 新字段留驻无害。
4. 测试与验收(O00 复现台账)
- SDK 单测(fake timers):PageContext 生命周期;归属快照不漂移(模拟 30min 空闲 + 延迟 flush + 会话切换);停留区间切分/并集(15s 周期、hidden/pagehide 共享结算、bfcache 恢复、路由切换);混合队列拆子批;beacon 字节预算;重试幂等。
- server 单测:v2 校验/落库、v1 兼容不回归、session_resume(含 prev 无效软处理)、page_stay 并集聚合(VisitSession.stay_ms、趋势聚合核对)、清洗三轨制(共享向量)、响应明细、event_id 唯一约束。
- 台账:
docs/验收记录-阶段A.md——OT01-OT04/OT06/OT17 用例与自动化测试映射表(OT01 30min 同页、OT02 多标签/SPA/bfcache/重试、OT03 时钟、OT06 合成敏感值、OT17 beacon unknown/部分拒绝),记录构建版本/输入/预期实际。 - 生产验证:部署后 48h 观察 D02 指标归零、清洗后 URL 无敏感值残留抽查。
5. 风险与边界
- 手机号藏于
?id=13800138000类参数:值判别无法与订单号区分,理论残余风险由规则版本化 + 阶段B 站点级配置兜底(文档口径:覆盖已知模式,不承诺理论完备)。 - 移动端后台冻结定时器:区间制天然正确(恢复时结算真实区间),真机抽检项。
- 多标签页:page_id 内存级互不干扰;共享 ctk_act 下标签间活动互续会话,符合"访客活跃"语义;停留按页面段累计,不称"访客去重参与时长"(文档 4.2)。
- 隐私模式 localStorage 失效:沿用 M1 内存降级(页面刷新视为新访客,已知限制,不在本阶段扩大)。