222990e587
CI / docs-build (push) Successful in 1m19s
- 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>
5.8 KiB
5.8 KiB
埋点分段持久化队列实施报告(M2 第二波)
作者:Frontend Developer(Flutter) 日期: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 analyze0 问题、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)