Files
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

69 lines
6.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.
# 埋点队列三项完善实施报告(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 P2A/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 @visibleForTesting``uploadBatch`:定时/退避测试以假上传子类替换网络层,在 `fakeAsync` 内驱动 `Timer.periodic`(真实 HttpServer 在假异步区无法完成 IO)。既有 HttpServer 集成测试不受影响,继续走真实 HTTP 路径。
## 3. 测试变化
- 基线 272 → **286 全绿**+14);`flutter analyze` 0 问题、`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 个既有测试文件