Files
fangke/docs/superpowers/specs/2026-09-18-collection-foundation-phase-a-design.md
T

12 KiB
Raw Blame History

采集底座修复(阶段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 清洗三轨制(替代现行"仅参数名黑名单"):

  1. 参数名黑名单(phone/mobile/tel/email/name/user/token/key/password/verif 等)→ 删;
  2. 归因白名单(utm_*、gclid、gbraid、wbraid、fbclid、gad_source、gad_campaignid、msclkid 等)→ 提取保留(供归因);
  3. 其余参数按值判别:含 @、超 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. 部署与灰度

  1. 服务端先上线(docker compose up -d --build web 自动 migrate)→ 线上 v1 上报回归验证(现有 142 测试 + 生产探活)。
  2. SDK 0.2.0 构建替换 sdk/dist(nginx 挂载 /opt/cms-cj/sdk/dist)→ 新访客逐步加载,旧缓存自然过期。
  3. 观察指标:孤立 page_stay 会话占比(D02 复现指标,应降为 0)、事件量增幅(15s 增量结算预计 +3~4 倍 page_stay,MySQL 万级/日无压力)、拒绝率、RawBatch 状态分布。
  4. 回滚安全: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 内存降级(页面刷新视为新访客,已知限制,不在本阶段扩大)。