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

5.8 KiB
Raw Permalink Blame History

埋点分段持久化队列实施报告(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.dart5 个,本地 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.darttest/analytics/analytics_event_store_test.darttest/analytics/analytics_persistent_queue_test.dart
  • 修改:lib/analytics/analytics_service.dart(接入持久化队列、分批冲刷)、lib/app/app.dart(冷启动 restore 挂接)
  • 依赖:无新增(shared_preferences ^2.5.4 已在 pubspec