docs: 阶段A spec 吸收审查修正——幂等约束已存在/sent_at已有/stay_ms重算式与v1v2混合解释

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 09:31:49 +08:00
parent 01cf26aabf
commit 8526ba5a97
@@ -31,7 +31,7 @@ M1 采集底座已在生产运行(hesucar.com,日访客 400+)。2026-09-16
### 1.1 PageContext(新模块 sdk/src/page.ts)
- **创建**:SDK init 时;SPA 路由切换时(见 1.3)。字段:`page_id`(uuid)、`session_id`(创建时冻结,不再变)、`visitor_id`、`started_at`、页内 `seq` 计数器。
- **创建**: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)时结算尾部停留。
@@ -43,7 +43,7 @@ M1 采集底座已在生产运行(hesucar.com,日访客 400+)。2026-09-16
- `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`。
- `sdk_loaded`:`sdk_version`(不再硬编码)、`build_id`、`schema_version`、`config_version`(构建时常量,当前固定 "1",随配置结构演进升级)。
### 1.3 SPA 路由切换
@@ -62,7 +62,7 @@ hook `history.pushState/replaceState` + `popstate`。**仅当规范化 path(
- flush 从队首事件快照取归属构造信封;队列存在多组 `(visitor, session, page)` 时**拆子批**按序发送,子批独立重试(重试重放原 body 字符串,event_id 不变)。
- 乱序到达是正常现象:服务端不拒,仅标记(见 2.3)。
- 批级信封新增:`schema_version:2`、`sent_at`(诊断时钟偏 vs 排队久的低成本依据)、`sdk_version`、`build_id`。
- 批级信封新增:`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 明细),内存累计计数。
@@ -77,7 +77,7 @@ esbuild `--define`:`__SDK_VERSION__`(package.json 0.2.0)、`__BUILD_ID__`
- 批级 `schema_version`:2 = 新路径;缺省 = v1(旧 SDK),照旧处理。
- 事件白名单 +`session_resume`;属性白名单扩展对应新字段。
- 事件级校验:site_id 匹配批 site、四元组完整、page_id uuid 格式、seq 软校验(乱序标记不拒绝)。
- **event_id 跨批幂等**(硬性验证项):确认 Event 是否有 `(site, event_id)` 唯一约束——若无则迁移补齐(迁移前先查生产是否已有重复数据,有则先清理策略再上约束)。
- event_id 跨批幂等:**已确认存在** `uq_site_event (site, event_id)` 唯一约束(models.py:105),重试重放双倍入库风险不成立,无需额外迁移。
### 2.2 模型迁移(models.py + migration)
@@ -92,7 +92,7 @@ esbuild `--define`:`__SDK_VERSION__`(package.json 0.2.0)、`__BUILD_ID__`
- **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 界面)。
- **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 不再隐含全量成功。