Files
patbond-doc/docs/development/iterations/iteration-3/10-analytics-queue-hardening.md
T
lixi f17e6f1215
CI / docs-build (push) Successful in 34s
docs: M3 第一波收口——报告 09~14 与契约草案入档挂导航
- 09 V5+community 骨架(191→206)、10 埋点队列三项(272→286)、
  11 契约草案(13 路径/19 操作)、12 防泄漏三仓落地、
  13 media MinIO 闭环 + auth 契约测试(→226,抓修 1 漂移)、14 收口总表
- backend-modules.md 更新五模块/六容器口径
- 媒体凭据形态已定型(契约冻结输入),剩余待定型点在 T3-04/05

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 17:13:49 +08:00

6.8 KiB
Raw Blame History

埋点队列三项完善实施报告(M3 第一波 T3-19 前端半边)

作者:Frontend DeveloperFlutter 日期:2026-09-08 依据:iteration-2/15-analytics-persistent-queue.md §4 遗留清单、iteration-3/06-experiment-tracking-plan.md §2.3(三项处置与优先级)、iteration-1/13-tracking-implementation-spec.md §3.3/§3.4 仓库:patbond-flutter dev 分支,提交 4d40c38(基线 720865b


1. 背景

15 号报告 §4 留下队列三个未做项:30 秒定时冲刷、失败退避、anonymousId 持久化。iteration-3 06 号 §2.3 把三项全部排入 M3(定时冲刷 P1:社区长前台会话最多积压 19 条不上传;anonymousId P2:A/B 前置 #4「登录前分流」硬依赖),ADR-020 拍板升为第一波必做。本波三项一次落地,既有语义(flushNow、4xx 毒丸丢弃、at-least-once 删段、按段拼批 ≤50、eventId UUIDv7)零回退。

2. 设计要点

2.1 30 秒定时冲刷(13 号 §3.4 第 4 触发点,四触发点补齐)

  • AnalyticsService 新增 startPeriodicFlush() / stopPeriodicFlush():前台期间 Timer.periodic(周期 flushInterval,默认 30 秒,构造参数化便于测试)触发 _flush()startPeriodicFlush 幂等(??=),不叠加定时器。
  • 生命周期挂接(app.dart + SessionTracker):
    • SessionTracker 新增 onEnterForeground 回调,复用既有「只在离开/回到 resumed 的第一次变更触发」的级联去重逻辑——回前台级联 hidden → inactive → resumed 只回调一次,冷启动首个 resumed(此前未离开过前台)不触发。
    • App initState 启动定时器;退后台回调改为「停定时器 + flushNow()」(既有退后台冲刷保留);回前台恢复定时器;App dispose 停定时器(widget 测试无悬挂 Timer)。
  • 与既有触发共存:满 20 条、退后台 flushNow、冷启动 restore 三个触发点原样保留;队列为空时定时器 tick 是廉价空转(takeBatch 为空即返回,无网络请求、无持久化写)。

2.2 失败指数退避(06 号 §2.3:客户端退避先行,不依赖后端限流)

  • 上传失败(网络错误/5xx)后进入退避:首次 30s,×2 递增(30s→60s→120s→240s),封顶 5 分钟;退避窗口内定时冲刷 tick 直接跳过,到点后下一 tick 重试。
  • 任一批上传拿到服务端应答(202 受理或 4xx 拒绝——连通性已恢复)即重置退避,恢复 30 秒节奏。
  • 退避只挡定时冲刷flushNow(退后台)、满 20 条、冷启动 restore 等显式触发不受限——退后台是最后的上传窗口,不能被退避挡掉。
  • 429 处理:从「4xx 毒丸丢弃」改为按网络错误同路径(保段 + 退避重试)。后端限流从未实现(iteration-2/09 出入项核实),Retry-After 精细分支待其落地后一并做,代码内已留注释说明。
  • 时钟经构造注入(now 参数,照 SessionTracker 先例),退避判定测试免真实等待。

2.3 anonymousId 持久化(13 号 §3.3 key,跨启动稳定)

  • 现状是每次冷启动 Uuid().v4() 随机生成,登录前事件无法跨启动归并。本波在 restore() 中增加采用/落盘:shared_preferences key pb.analytics.anonymousId 已有值则采用;无值则把本次构造生成的 v4 落盘——首次生成后跨冷启动稳定。
  • 读取/写入失败(持久化不可用)降级为进程内临时 id,只打日志绝不抛出(埋点旁路原则),埋点照常入队。
  • 构造显式注入 anonymousId 的测试通道不参与持久化采用/落盘,既有测试语义(anon-123 断言)不受影响。
  • 已知边界:restore() 完成前 track 的事件仍带构造时的临时 id(首启时两者同值无影响;后续启动 app 装配层在挂接 tracker 前即调用 restore,实际窗口趋近于零),记录备查。

2.4 可测性改造

_upload 提升为 @protected @visibleForTestinguploadBatch:定时/退避测试以假上传子类替换网络层,在 fakeAsync 内驱动 Timer.periodic(真实 HttpServer 在假异步区无法完成 IO)。既有 HttpServer 集成测试不受影响,继续走真实 HTTP 路径。

3. 测试变化

  • 基线 272 → 286 全绿+14);flutter analyze 0 问题、dart format 无 diff。
  • 新增 test/analytics/analytics_flush_scheduler_test.dart8 个,fakeAsync + 时钟注入 + 假上传子类):29 秒不触发 / 30 秒冲刷不满额队列、空队列不发起上传、stop 停 start 恢复(退后台/回前台)、start 幂等不叠加、30s→60s→120s 退避序列且窗口内 tick 跳过、退避封顶 5 分钟(240s×2 → 300s)、退避期间 flushNow 不受限、成功重置退避恢复 30 秒节奏。
  • analytics_persistent_queue_test.dart +4:429 保段不丢弃不计丢弃数;anonymousId 首次 restore 落盘、冷启动新实例沿用存储值且事件携带、构造注入通道不被覆盖。
  • analytics_service_test.dart +1:持久化不可用时 restore 降级临时 id 不崩溃。
  • session_tracker_test.dart +1:前后台回调级联下成对各触发一次,重复 resumed 不触发。
  • 既有语义回归零改动:4xx 毒丸、at-least-once、40+20 分批、flushNow 等原测试全部原样通过。
  • 依赖:dev_dependencies 显式声明 fake_async ^1.3.3flutter_test 既有传递依赖,无新增第三方)。

4. 与 15 号 §4 遗留清单对照

15 号 §4 未做项 本波状态 说明
30 秒定时冲刷 已做 前台 Timer.periodic,退后台停/回前台恢复;四触发点补齐
指数退避 已做 30s ×2 封顶 5min,只挡定时冲刷,成功即重置;15 号原案「5s ×2」按 T3-19 拍板参数调整为 30s 起步
429 按 Retry-After 部分(范围内的全部) 429 已从毒丸丢弃改为保段退避;Retry-After 精细分支依赖后端限流(09 号出入项,未实现),随其落地一并做
pb.analytics.anonymousId 持久化 已做 首次生成落盘、跨启动稳定、失败降级临时 id
lastActiveAt 持久化 不做(维持决策) 03 号评估 §3.1 已裁定会话纯内存方案,非遗留项
401 去 Authorization 重试一次 未做 不在 T3-19 三项范围,继续遗留

5. 交付物

  • 代码:patbond-flutter dev 提交 4d40c38(已推送),改动 9 文件 +445/−14。
  • 新增:test/analytics/analytics_flush_scheduler_test.dart
  • 修改:lib/analytics/analytics_service.dart(定时器、退避、anonymousId 持久化、uploadBatch 可测性)、lib/analytics/session_tracker.dartonEnterForeground)、lib/app/app.dart(定时器生命周期装配)、pubspec.yaml/pubspec.lockfake_async 显式声明)、3 个既有测试文件