From 01cf26aabfac2afc0bf5dae41c3c02ab2f1500e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 18 Sep 2026 09:25:56 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E9=87=87=E9=9B=86=E5=BA=95=E5=BA=A7?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=98=B6=E6=AE=B5A=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=88=E5=BD=92=E5=B1=9E=E5=BF=AB=E7=85=A7?= =?UTF-8?q?/=E5=8C=BA=E9=97=B4=E5=81=9C=E7=95=99/=E6=B8=85=E6=B4=97?= =?UTF-8?q?=E4=B8=89=E8=BD=A8=E5=88=B6/=E5=A5=91=E7=BA=A6=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- ...18-collection-foundation-phase-a-design.md | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-18-collection-foundation-phase-a-design.md diff --git a/docs/superpowers/specs/2026-09-18-collection-foundation-phase-a-design.md b/docs/superpowers/specs/2026-09-18-collection-foundation-phase-a-design.md new file mode 100644 index 0000000..269f546 --- /dev/null +++ b/docs/superpowers/specs/2026-09-18-collection-foundation-phase-a-design.md @@ -0,0 +1,132 @@ +# 采集底座修复(阶段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`(创建时冻结,不再变)、`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.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`、`sent_at`(诊断时钟偏 vs 排队久的低成本依据)、`sdk_version`、`build_id`。 +- **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 跨批幂等**(硬性验证项):确认 Event 是否有 `(site, event_id)` 唯一约束——若无则迁移补齐(迁移前先查生产是否已有重复数据,有则先清理策略再上约束)。 + +### 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 按 `(page_id)` 分组做**区间并集**后求和——不按 SUM(active_ms) 累加(15s 增量会重复计)。**受影响聚合点核查清单**:VisitSession.stay_ms、趋势/总览实时聚合(35f7b55)、访客详情时间线与停留显示——全部核对为并集/峰值语义,保证 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(≤64 字母数字连字符)→ 保留(未知业务参数如 ?id=123 自动保住,WordPress query 路由不受影响)。 + +另: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 内存降级(页面刷新视为新访客,已知限制,不在本阶段扩大)。