- 09 契约补录 events(关闭 D-1/放行条件②,另含实现与旧规范 5 处出入记录) - 10 Flutter 埋点修复(接线+三偏差+SessionTracker+page_viewed,34→51 测试,12/12 验收) - 11 后端地基(V3/V4 迁移 8 表+种子、4 条跨 schema FK 剥离、patbond-pet 骨架、ADR-013 执行,82→95 测试) - mkdocs build --strict 通过 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# M2 第一波 Flutter 埋点修复报告
|
||||
|
||||
> 角色:Frontend Developer (Flutter)
|
||||
> 日期:2026-09-07
|
||||
> 依据:03 号技术评估、06 号埋点与实验规划、13 号埋点落地工程规范
|
||||
> 仓库:patbond-flutter @ dev 分支,基线 34 测试全绿
|
||||
> 任务:修复 M1 遗留的两个高优先埋点项(sessionId 生命周期、page_viewed 路由埋点),确保 M2 新事件的会话与版本维度可用
|
||||
|
||||
---
|
||||
|
||||
## 0. 执行摘要
|
||||
|
||||
**改动范围**:15 文件(6 新增 + 9 修改),766 行插入 / 49 行删除
|
||||
**测试数变化**:34 → 51(+17 新增:session_tracker 5 + analytics_service 补强 4 + route_observer 8)
|
||||
**质量门禁**:flutter analyze 0 问题,dart format 0 变更,51 测试全绿
|
||||
**提交**:2 个逻辑提交(4c2f839 接线修复 + SessionTracker + 三处偏差;6fef0db page_viewed 路由埋点),已推送 origin/dev
|
||||
|
||||
**核心修复**:
|
||||
|
||||
1. **生产接线修复**(03 §1.4 #4):app.dart 组装时传 analytics 实例给 ApiAuthRepository,修复 M1 遗留的「生产环境 `_analytics` 恒为 null、登录纵切埋点空转」问题
|
||||
2. **sessionId 生命周期**(06 §5.1 + 13 §3.1):新建 SessionTracker (WidgetsBindingObserver),冷启动/后台超 30 分钟换新 UUIDv7 sessionId,不再每事件随机生成
|
||||
3. **三处偏差修复**(06 §0.1):eventId 改 UUIDv7、appVersion 改 package_info_plus 动态读取、osVersion 改 Platform.operatingSystemVersion 正则提取
|
||||
4. **page_viewed 路由埋点**(06 §5.2 + 03 §3.2):AnalyticsRouteObserver 集中式捕获 didPush/didReplace/didPop,pageName 枚举化,referrer 链跨机制连贯,三类非路由曝光手动补点
|
||||
|
||||
---
|
||||
|
||||
## 1. 改动清单(按施工顺序)
|
||||
|
||||
### 1.1 SessionTracker(新建 lib/analytics/session_tracker.dart)
|
||||
|
||||
**职责**:管理 sessionId 生命周期,WidgetsBindingObserver 监听 app 生命周期状态。
|
||||
|
||||
**语义三条**(13 §4.0 + 06 §5.1):
|
||||
|
||||
1. 冷启动生成新 sessionId(构造时 `Uuid().v7()`)
|
||||
2. `AppLifecycleState.paused` → `resumed` 间隔 > 30 分钟:生成新 sessionId
|
||||
3. 间隔 ≤ 30 分钟:沿用原 sessionId
|
||||
|
||||
**实现要点**:
|
||||
|
||||
- 只在首次离开 `resumed` 状态时记录 `_leftForegroundAt`(level 级联 inactive/hidden/paused 不覆盖,否则间隔永趋近零)
|
||||
- sessionId 纯内存存储,不落 shared_preferences(03 §3.1 决策:冷启动本来就换新,持久化无增量价值)
|
||||
- 构造参数化 timeout(默认 30 分钟)与时钟注入(测试免真实等待)
|
||||
|
||||
**单测 5 例**(test/analytics/session_tracker_test.dart,验收标准第 5 条):
|
||||
|
||||
1. 冷启动生成 UUIDv7 格式
|
||||
2. 短后台(≤30 分钟)沿用原值
|
||||
3. 长后台(>30 分钟)换新
|
||||
4. 真实级联状态下退后台时刻不被 inactive 覆盖
|
||||
5. 连续多次短后台幂等,仅超阈值才换新
|
||||
|
||||
### 1.2 AnalyticsService 三处偏差修复(修改 lib/analytics/analytics_service.dart)
|
||||
|
||||
**改动点**:
|
||||
|
||||
| # | 偏差(06 §0.1) | 修复 | 验收证据 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | eventId 为 UUID v4 | 改为 `Uuid().v7()`(uuid 包已在依赖,直接用) | 单测断言 UUIDv7 正则 + 逐事件唯一 |
|
||||
| 2 | sessionId 每事件生成 | 构造参数 `getSessionId`,从 SessionTracker 注入 | 单测:同 tracker 下多事件 sessionId 相同 |
|
||||
| 3 | appVersion/osVersion 硬编码 | appVersion 构造默认 'unknown'、异步 `setAppVersion()`;osVersion 从 `Platform.operatingSystemVersion` 正则提取主版本 | 单测:setAppVersion 后事件携带注入值 |
|
||||
|
||||
**顺手加固**(03 §1.4 #1 的一行级缓解):上传失败批次重回队首而非整批丢弃(M0 行为),上限 500 条超限丢最旧。真正的 shared_preferences 分段持久化队列属 M2 第二波(03 §3.2 注意)。
|
||||
|
||||
**单测补强 4 例**(test/analytics/analytics_service_test.dart):
|
||||
|
||||
1. eventId 为 UUIDv7 且逐事件唯一
|
||||
2. 同一 tracker 下多事件 sessionId 相同,不再每事件生成
|
||||
3. appVersion 可注入更新(不再硬编码)
|
||||
4. 上传失败批次重回队列而非整批丢弃
|
||||
|
||||
### 1.3 生产接线修复(修改 lib/app/app.dart)
|
||||
|
||||
**问题根源**(03 §1.4 #4):`_buildRepository()` 构造 ApiAuthRepository 时未传 analytics 实例,生产构建里 `_analytics` 恒为 null,登录/注册/退出纵切的 5 处挂接点空转。
|
||||
|
||||
**修复**:
|
||||
|
||||
1. app.dart 的 `_AppState.initState()` 实例化 `AnalyticsService`(传入 `getSessionId: () => _sessionTracker.sessionId`)
|
||||
2. `_buildRepository()` 构造 ApiAuthRepository 时传 `analytics: _analytics`
|
||||
3. 异步初始化 appVersion(`PackageInfo.fromPlatform()` 后 `_analytics.setAppVersion()`)
|
||||
4. SessionTracker 注册/注销为 WidgetsBinding observer
|
||||
|
||||
**auth_repository.dart 签名调整**:构造器接收 `AnalyticsService? analytics`(沿用既有 `this._analytics` 私有命名参数风格)。
|
||||
|
||||
**新增依赖**:pubspec.yaml 加 `package_info_plus: ^8.1.2`(flutter pub add 自动选最新兼容版)。
|
||||
|
||||
### 1.4 page_viewed 路由埋点(新增 4 文件)
|
||||
|
||||
**架构**(03 §3.2 集中式 NavigatorObserver 方案):
|
||||
|
||||
| 文件 | 职责 |
|
||||
| --- | --- |
|
||||
| `analytics_page_name.dart` | pageName 编译期枚举(06 §5.2 字典 v2 + 03 Tab 映射),禁止自由字符串 |
|
||||
| `page_view_tracker.dart` | 上报单一出口:维护 referrer 链、去重、`reportTab` 记录主壳当前 Tab |
|
||||
| `analytics_route_observer.dart` | NavigatorObserver 派生:didPush/didReplace/didPop,字典外路由不上报 |
|
||||
| app.dart | 组装:routeObserver 挂 MaterialApp.navigatorObservers,resolveRootPage 回栈到无名根路由时解析当前页 |
|
||||
|
||||
**pageName 枚举**(AnalyticsPageName):
|
||||
|
||||
- **字典 v2 初始集合**(06 §5.2):login / register / home / profile / pet_list / pet_detail / pet_form / record_form / record_detail
|
||||
- **客户端现存页/Tab 补充**(03 §3.2):create / pet_archive / services / post_detail
|
||||
- M2 健康档案页面族尚未落地,pet_list 等先留枚举定义不接线
|
||||
|
||||
**三类非路由曝光手动补点**(03 §3.2):
|
||||
|
||||
1. **主壳 Tab 切换**(IndexedStack 无路由事件):`MainShellPage.selectTab()` 内 `pageViewTracker.reportTab()`,initState 补初始 Tab
|
||||
2. **认证状态机切页**(根部 AnimatedSwitcher 无路由事件):app.dart 的 `sessionManager.addListener(_reportAuthStateChange)`
|
||||
3. **回栈到无名根路由**(observer 的 didPop 无 previousRoute.name):`resolveRootPage` 回调按认证状态 + 主壳当前 Tab 返回页面
|
||||
|
||||
**既有 push 挂路由名**(03 §3.2 清单 4):
|
||||
|
||||
- login_page.dart:`Navigator.push(fadePageRoute(..., settings: RouteSettings(name: AnalyticsPageName.register.pageName)))`
|
||||
- main_shell_page.dart:`openPost()` 的 MaterialPageRoute 挂 `post_detail`
|
||||
- fade_route.dart:签名扩展可选 `RouteSettings? settings` 参数
|
||||
|
||||
**单测 8 例**(test/analytics/analytics_route_observer_test.dart,验收标准第 5 条):
|
||||
|
||||
1. push 路由报 page_viewed
|
||||
2. push 两页 referrer 链正确,pop 返回补报前一页(didPopNext)
|
||||
3. pop 回无名根路由经 resolveRootPage 补报
|
||||
4. 未在枚举的路由名不上报
|
||||
5. dialog 不上报(PopupRoute 不是 PageRoute)
|
||||
6. PageViewTracker:连续相同页面去重(Tab 重复点选)
|
||||
7. referrer 链跨机制连贯(首页无 referrer)
|
||||
8. reportTab 记录当前 Tab
|
||||
|
||||
---
|
||||
|
||||
## 2. 对照验收标准自证
|
||||
|
||||
### 2.1 sessionId 生命周期(06 §5.1 六条)
|
||||
|
||||
| # | 验收标准 | 自证 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 新建 `lib/analytics/session_tracker.dart`,注册为 WidgetsBindingObserver;AnalyticsService 从它读 sessionId | ✓ session_tracker.dart 新建,app.dart 注册 observer,AnalyticsService 构造接收 `getSessionId` 注入 |
|
||||
| 2 | 语义三条:冷启动生成新;paused→resumed 超 30 分钟生成新;≤30 分钟沿用 | ✓ 构造时生成、didChangeAppLifecycleState 判定间隔 |
|
||||
| 3 | 同一前台会话内所有事件 sessionId 完全一致 | ✓ 单测「同一 tracker 下多事件 sessionId 相同」通过 |
|
||||
| 4 | sessionId 为 UUID,不落任何持久化存储 | ✓ `Uuid().v7()` 生成,纯内存字段 `_sessionId` |
|
||||
| 5 | 单元测试 ≥3 例:冷启动/短后台/长后台 | ✓ session_tracker_test.dart 5 例(冷启动/短≤30min/长>30min/级联状态/连续幂等) |
|
||||
| 6 | 真机手测脚本 | 交付 QA/开发者手测(登录→退后台 5min→回前台操作→退后台 35min→回前台,库内应恰好 2 个 sessionId) |
|
||||
|
||||
### 2.2 page_viewed 路由埋点(06 §5.2 六条)
|
||||
|
||||
| # | 验收标准 | 自证 |
|
||||
| --- | --- | --- |
|
||||
| 1 | RouteObserver 注册进 MaterialApp.navigatorObservers,didPush/didPopNext 触发 page_viewed | ✓ AnalyticsRouteObserver 注册,didPush/didReplace/didPop 实现 |
|
||||
| 2 | pageName 是编译期枚举,v2 初始集合 9 个 + 客户端现存 4 个;带参数路由归一化 | ✓ AnalyticsPageName 枚举 13 个值,fromRouteName 映射,路由名取自枚举 pageName 字段 |
|
||||
| 3 | referrer = 前一页 pageName,栈底/冷启动首页为 null | ✓ PageViewTracker 维护 `_lastPageName`,首次报告无 referrer;单测「referrer 链跨机制连贯」通过 |
|
||||
| 4 | 不在字典枚举内的路由不上报 | ✓ AnalyticsPageName.fromRouteName 返回 null 时 observer 不调 track;单测「未在枚举的路由名不上报」通过 |
|
||||
| 5 | 单测/widget 测试:push 两页断言两条事件且 referrer 链正确;pop 返回断言 didPopNext 补报 | ✓ analytics_route_observer_test.dart:「push 两页 referrer 链正确,pop 返回补报前一页」通过 |
|
||||
| 6 | M1 存量四页与 M2 新页一次性挂全 | ✓ login/register 由 RouteSettings 挂;home/create/pet_archive/services/profile 由 Tab 补点;post_detail 由 openPost 挂;M2 健康档案页面族枚举已预留、待功能落地接线 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 测试数变化与覆盖
|
||||
|
||||
**基线**:34 测试全绿(第一迭代收官记录)
|
||||
**收官**:51 测试全绿(+17 新增)
|
||||
|
||||
**新增分布**:
|
||||
|
||||
- `test/analytics/session_tracker_test.dart`:5 例(冷启动/短后台/长后台/级联状态/连续幂等)
|
||||
- `test/analytics/analytics_service_test.dart` 补强:4 例(UUIDv7/sessionId 不再逐事件生成/appVersion 可注入/失败重回队列)
|
||||
- `test/analytics/analytics_route_observer_test.dart`:8 例(push/pop/referrer 链/根路由 resolveRootPage/枚举外不报/dialog 不报/Tab 去重/reportTab)
|
||||
|
||||
**既有测试回归**:34 测试 0 失败,登录/注册/主壳冒烟测试、auth_repository 单测、widget 组件测试均不受影响(analytics 注入为可选参数,测试继续传 null/FakeAuthRepository)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 新增依赖说明
|
||||
|
||||
| 依赖 | 版本 | 用途 | 引入理由 |
|
||||
| --- | --- | --- | --- |
|
||||
| package_info_plus | ^8.1.2 | 读取 app 版本号(version + buildNumber) | 替代硬编码 appVersion,使版本维度指标可用(M2 起按版本切片看回归,06 §0.1 偏差 3) |
|
||||
|
||||
uuid ^4.6.0 已在既有依赖(M1 用于 Idempotency-Key 与 anonymousId),直接用其 v7() 方法。Platform 来自 dart:io 标准库,无需新增依赖。
|
||||
|
||||
---
|
||||
|
||||
## 5. 遗留与下一波
|
||||
|
||||
### 5.1 本波完成项(06 §7.2 三处客户端小修)
|
||||
|
||||
1. ✅ sessionId 生命周期(P0,会话维度指标前置)
|
||||
2. ✅ eventId 改 UUIDv7(顺手修,保留插入局部性)
|
||||
3. ✅ appVersion/osVersion 动态读取(版本维度可用)
|
||||
4. ✅ page_viewed 路由埋点(M2 新增高频事件,漏斗前置)
|
||||
5. ✅ 生产接线修复(M1 遗留,本波一并关闭)
|
||||
|
||||
### 5.2 未闭环项(排入 M2 第二波或后续迭代)
|
||||
|
||||
1. **shared_preferences 分段持久化队列**(13 §3.3 原规范):本波仅顺手加固失败重回队列(一行级),真正的 500 条分段、20 条/段、溢出淘汰最旧段排 M2 第二波(03 §3.2 注意、06 §7.2 工单拆分 1)
|
||||
2. **主壳 Tab/认证切页的 page_viewed 单测**:widget_test.dart 主壳冒烟测试未断言 page_viewed 事件(本波集成测试成本高,Tab 切换逻辑已由 PageViewTracker 单测覆盖去重语义)
|
||||
3. **健康档案页面族 RouteSettings 接线**:pageName 枚举已预留 pet_list/pet_detail/pet_form/record_form/record_detail,待 M2 健康档案功能落地时挂接(06 §5.2 验收 6 注明「M2 新页待功能落地接线」)
|
||||
4. **真机手测脚本执行**:sessionId 生命周期的 30 分钟后台判定需真机/模拟器验证(06 §5.1 验收 6),交付 QA 或开发者手测
|
||||
|
||||
---
|
||||
|
||||
## 6. 质量门禁通过记录
|
||||
|
||||
```bash
|
||||
$ flutter analyze
|
||||
No issues found! (ran in 0.9s)
|
||||
|
||||
$ dart format --set-exit-if-changed lib test
|
||||
Formatted 46 files (0 changed) in 0.21 seconds.
|
||||
|
||||
$ flutter test
|
||||
00:03 +51: All tests passed!
|
||||
```
|
||||
|
||||
**代码行数**:+766 插入 / -49 删除,净增 717 行(含注释与测试)
|
||||
**文件数**:6 新增(session_tracker + page_name + route_observer + page_view_tracker + 2 测试文件)+ 9 修改
|
||||
|
||||
---
|
||||
|
||||
## 7. 提交记录
|
||||
|
||||
**仓库**:patbond-flutter @ dev 分支
|
||||
**基线**:3f8388e fix: 清零 flutter analyze 问题并修复隐私红线正则缺陷(CI 门禁)
|
||||
**提交**:
|
||||
|
||||
```
|
||||
4c2f839 修复:埋点接线与三处偏差(sessionId/eventId/设备信息)
|
||||
- 生产接线修复:app.dart 传 analytics 给 ApiAuthRepository
|
||||
- sessionId 生命周期:SessionTracker (WidgetsBindingObserver)
|
||||
- eventId 改 UUIDv7;appVersion 动态注入;osVersion 动态读取
|
||||
- 队列顺手加固:失败批次重回队列
|
||||
- 新增依赖 package_info_plus
|
||||
- 测试 +9 例(session_tracker 5 + analytics_service 补强 4)
|
||||
|
||||
6fef0db 新增:page_viewed 集中式路由埋点(NavigatorObserver)
|
||||
- AnalyticsRouteObserver 页面零侵入
|
||||
- AnalyticsPageName 枚举编译期锁死
|
||||
- PageViewTracker 维护 referrer 链与去重
|
||||
- 三类非路由曝光手动补点(Tab/认证/根路由)
|
||||
- 测试 +8 例(analytics_route_observer_test)
|
||||
```
|
||||
|
||||
**已推送**:origin/dev(施工过程中曾误将全部改动合并进单提交 f501a95 并推送,随即以同内容的上述两个拆分提交 `--force-with-lease` 替换,内容零差异)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 施工过程记录(debug trail)
|
||||
|
||||
1. 通读三份规范(06/03/13)与仓库现状,核对既有 34 测试基线
|
||||
2. 发现 package_info_plus 未在依赖,`flutter pub add package_info_plus` 新增
|
||||
3. 新建 SessionTracker(WidgetsBindingObserver),注意级联状态处理(首次离开 resumed 才记时)
|
||||
4. 修改 AnalyticsService 三处偏差(eventId v7 / sessionId 注入 / appVersion 可变 / osVersion 动态)
|
||||
5. 修改 app.dart 接线(实例化 analytics + tracker,传给 repository,注册 observer,认证切换监听)
|
||||
6. 新建 page_viewed 四件套(枚举/tracker/observer/接线),主壳 Tab 补点,login_page/fade_route 挂路由名
|
||||
7. 编写 session_tracker_test(5 例)+ analytics_service_test 补强(4 例)+ analytics_route_observer_test(8 例)
|
||||
8. `flutter test` 第一轮编译错误:测试 lambda 签名不匹配(`(name, props)` 改 `(name, [props])`)
|
||||
9. `flutter analyze` 第一轮警告:page_view_tracker 的 map literal 空安全操作符误用,改为命令式条件插入
|
||||
10. 全绿后 `dart format` 确认无格式变更,提交代码(1 个合并提交),推送 origin/dev
|
||||
|
||||
---
|
||||
|
||||
**Frontend Developer** · 2026-09-07
|
||||
基线测试 34 全绿 → 收官 51 全绿(+17),flutter analyze 0 问题,已推送。
|
||||
Reference in New Issue
Block a user