f17e6f1215
CI / docs-build (push) Successful in 34s
- 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>
6.8 KiB
6.8 KiB
埋点队列三项完善实施报告(M3 第一波 T3-19 前端半边)
作者:Frontend Developer(Flutter) 日期: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()」(既有退后台冲刷保留);回前台恢复定时器;Appdispose停定时器(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 keypb.analytics.anonymousId已有值则采用;无值则把本次构造生成的 v4 落盘——首次生成后跨冷启动稳定。 - 读取/写入失败(持久化不可用)降级为进程内临时 id,只打日志绝不抛出(埋点旁路原则),埋点照常入队。
- 构造显式注入
anonymousId的测试通道不参与持久化采用/落盘,既有测试语义(anon-123断言)不受影响。 - 已知边界:
restore()完成前 track 的事件仍带构造时的临时 id(首启时两者同值无影响;后续启动 app 装配层在挂接 tracker 前即调用 restore,实际窗口趋近于零),记录备查。
2.4 可测性改造
_upload 提升为 @protected @visibleForTesting 的 uploadBatch:定时/退避测试以假上传子类替换网络层,在 fakeAsync 内驱动 Timer.periodic(真实 HttpServer 在假异步区无法完成 IO)。既有 HttpServer 集成测试不受影响,继续走真实 HTTP 路径。
3. 测试变化
- 基线 272 → 286 全绿(+14);
flutter analyze0 问题、dart format无 diff。 - 新增
test/analytics/analytics_flush_scheduler_test.dart(8 个,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.3(flutter_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.dart(onEnterForeground)、lib/app/app.dart(定时器生命周期装配)、pubspec.yaml/pubspec.lock(fake_async 显式声明)、3 个既有测试文件