Files
lixi 222990e587
CI / docs-build (push) Successful in 1m19s
docs: M2 第二波收口——报告 13~21 与契约草案入档挂导航
- 13~18 后端纵切六单报告(T2-03~08,测试 95→182)
- 14 + openapi-pets-draft.yaml 契约起草档案
- 19 契约冻结报告(v1.2.0,22 项草案修正对照)
- 20 契约一致性测试(快照机制 + 1 漂移修复)
- 21 第二波收口总表(定型语义汇总,第三波接入依据)
- mkdocs build --strict 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 10:40:27 +08:00

72 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 埋点分段持久化队列实施报告(M2 第二波)
> 作者:Frontend DeveloperFlutter
> 日期:2026-09-07
> 依据:`iterations/iteration-1/13-tracking-implementation-spec.md` §3.3/§3.4(分段队列原始设计)、`iteration-2/06-experiment-tracking-plan.md` §基础设施评估(约两周离线积压容量)、`iteration-2/10-flutter-analytics-repair.md`(第一波修复语义基线)
> 仓库:patbond-flutter dev 分支,提交 `33b993c`(基线 `1afec6a`
---
## 1. 背景
第一波按计划只做了内存队列的一行级加固(失败重回队列、上限 500 丢最旧),分段持久化推迟到本波。本波将队列升级为 13 号规范 §3.3 的 shared_preferences 分段持久化方案:应用被杀/冷启动不再丢失未上传事件,离线积压容量约两周(500 条上限,06 号报告估算)。
## 2. 设计要点
### 2.1 存储布局(新文件 `lib/analytics/analytics_event_store.dart`
按 13 号规范 §3.3 的 key 布局实现:
| Key | 内容 |
| --- | --- |
| `pb.analytics.segIndex` | JSON 数组:段 ID 有序列表(旧 → 新) |
| `pb.analytics.seg.<segId>` | JSON 数组:该段最多 20 条序列化事件 |
| `pb.analytics.droppedCount` | 本地累计丢弃计数(溢出淘汰 + 4xx 丢批 + 损坏段),诊断用 |
- **写入**`trackEvent` 追加到当前开放段并只重写该段(≤ 20 条、几 KB),避免整队列单 key 的 O(n) 重写放大;段满 20 条封段、开新段。
- **上限与淘汰**:总量 500 条(25 段),超限丢最旧整段并累加 `droppedCount`
- **at-least-once**:上传拿到终态才删段——202 受理删段,4xx 永久拒绝删段并计入丢弃数;网络错误/5xx 段原样保留在本地。应用在响应前被杀,事件仍在,冷启动重发,服务端靠 eventId(UUIDv7)幂等去重。
- **内存为唯一事实来源**shared_preferences 是尽力而为的镜像,持久化不可用(如插件未初始化)时降级纯内存队列,任何存取失败只打日志绝不抛出(埋点旁路原则)。
### 2.2 并发与损坏容错
- **冲刷中新事件不丢**`takeBatch` 取最旧整段拼批时即封段(sealed),上传在途期间新事件只会写入新的开放段;批内容与对应段不再变化,202 后整段删除安全。
- **损坏段**:JSON 解析失败的段直接删 key 丢弃、计入 `droppedCount`,恢复流程不崩溃;段索引本身损坏时按 key 前缀清扫孤儿段后从空队列重建。
- **恢复顺序**:restore 前已入队的内存事件排在恢复事件之后(恢复的更旧,优先上传/淘汰),并在恢复时补落盘。
### 2.3 服务接入(`lib/analytics/analytics_service.dart` + `lib/app/app.dart`
- 冲刷触发点保持不变:满 20 条 + 离开前台 `flushNow()`;新增冷启动 `restore()`app.dart initState 后台调用,不阻塞渲染)恢复积压并冲刷一次——即 13 号 §3.4 四个触发点落地三个(30 秒定时器仍未做,见 §4)。
- 冲刷改为按段拼批 ≤ 50 条循环上传,对齐契约单批上限(13 号 §1.1;旧实现失败重回后可能单批远超 50 被服务端整批 400 拒绝,本波顺带修复)。
- 第一波语义无回退:`flushNow()`、4xx 毒丸丢弃、eventId UUIDv7、sessionId 注入、`_platformName()` 均保留。
## 3. 测试变化
- 基线 51 → **64 全绿**+13);`flutter analyze` 0 问题、`dart format` 无 diff。
- 新增 `test/analytics/analytics_event_store_test.dart`(8 个):持久化恢复与分段数、501 条触发丢最旧整段、损坏段容错与索引清理、索引损坏清扫重建、封段隔离在途批次、按段拼批 ≤50、删段后 prefs 无残留、无持久化降级纯内存。
- 新增 `test/analytics/analytics_persistent_queue_test.dart`5 个,本地 HttpServer 模拟 202/400):满 20 冲刷且 202 后清段、flushNow 冲刷不满额队列、上传失败持久化 + 冷启动恢复自动重传、4xx 删段丢弃计数、60 条积压按 40+20 分批上传。
- 既有 8 个 AnalyticsService 测试未改动全部通过(`pendingEvents` 语义兼容)。
## 4. 与 13 号规范符合度对照
| 规范条目(§3.3/§3.4) | 状态 | 说明 |
| --- | --- | --- |
| 分段存储 key 布局(segIndex / seg.\<id\> / droppedCount | 符合 | key 名与规范一致 |
| 每段 ≤ 20 条、写入只重写当前段 | 符合 | |
| 总上限 500 条、超限丢最旧整段 | 符合 | |
| 202 后才删段(at-least-once) | 符合 | 取整段组批,无部分消费段重写的需要 |
| 单批 ≤ 50 条 | 符合 | 每批最多 2 整段(40 条),循环冲刷 |
| 冷启动恢复 + 冲刷触发 | 符合 | `restore()` 于 app 启动挂接 |
| 满 20 条 / 退后台冲刷触发 | 符合 | 第一波语义保留 |
| `pb.analytics.anonymousId` / `lastActiveAt` 持久化 | 未做 | anonymousId 仍每冷启动重新生成,属会话/身份持久化范畴,非本工单队列范围,建议下波补 |
| 30 秒定时冲刷 | 未做 | 本波任务明确保持触发点不变;低活跃场景已由退后台 + 冷启动冲刷兜底 |
| 指数退避(5s ×2 上限 5min)、429 按 Retry-After | 未做 | 沿用第一波语义:4xx(含 429)一律永久丢弃;有限流上量前风险低,遗留下波 |
| 401 去 Authorization 重试一次 | 未做 | 第一波遗留项,本波未扩展 |
## 5. 交付物
- 代码:patbond-flutter `dev` 提交 `33b993c`(已推送),改动 5 文件 +577/−28。
- 新增:`lib/analytics/analytics_event_store.dart``test/analytics/analytics_event_store_test.dart``test/analytics/analytics_persistent_queue_test.dart`
- 修改:`lib/analytics/analytics_service.dart`(接入持久化队列、分批冲刷)、`lib/app/app.dart`(冷启动 restore 挂接)
- 依赖:无新增(`shared_preferences ^2.5.4` 已在 pubspec