- 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>
15 KiB
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
核心修复:
- 生产接线修复(03 §1.4 #4):app.dart 组装时传 analytics 实例给 ApiAuthRepository,修复 M1 遗留的「生产环境
_analytics恒为 null、登录纵切埋点空转」问题 - sessionId 生命周期(06 §5.1 + 13 §3.1):新建 SessionTracker (WidgetsBindingObserver),冷启动/后台超 30 分钟换新 UUIDv7 sessionId,不再每事件随机生成
- 三处偏差修复(06 §0.1):eventId 改 UUIDv7、appVersion 改 package_info_plus 动态读取、osVersion 改 Platform.operatingSystemVersion 正则提取
- 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):
- 冷启动生成新 sessionId(构造时
Uuid().v7()) AppLifecycleState.paused→resumed间隔 > 30 分钟:生成新 sessionId- 间隔 ≤ 30 分钟:沿用原 sessionId
实现要点:
- 只在首次离开
resumed状态时记录_leftForegroundAt(level 级联 inactive/hidden/paused 不覆盖,否则间隔永趋近零) - sessionId 纯内存存储,不落 shared_preferences(03 §3.1 决策:冷启动本来就换新,持久化无增量价值)
- 构造参数化 timeout(默认 30 分钟)与时钟注入(测试免真实等待)
单测 5 例(test/analytics/session_tracker_test.dart,验收标准第 5 条):
- 冷启动生成 UUIDv7 格式
- 短后台(≤30 分钟)沿用原值
- 长后台(>30 分钟)换新
- 真实级联状态下退后台时刻不被 inactive 覆盖
- 连续多次短后台幂等,仅超阈值才换新
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):
- eventId 为 UUIDv7 且逐事件唯一
- 同一 tracker 下多事件 sessionId 相同,不再每事件生成
- appVersion 可注入更新(不再硬编码)
- 上传失败批次重回队列而非整批丢弃
1.3 生产接线修复(修改 lib/app/app.dart)
问题根源(03 §1.4 #4):_buildRepository() 构造 ApiAuthRepository 时未传 analytics 实例,生产构建里 _analytics 恒为 null,登录/注册/退出纵切的 5 处挂接点空转。
修复:
- app.dart 的
_AppState.initState()实例化AnalyticsService(传入getSessionId: () => _sessionTracker.sessionId) _buildRepository()构造 ApiAuthRepository 时传analytics: _analytics- 异步初始化 appVersion(
PackageInfo.fromPlatform()后_analytics.setAppVersion()) - 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):
- 主壳 Tab 切换(IndexedStack 无路由事件):
MainShellPage.selectTab()内pageViewTracker.reportTab(),initState 补初始 Tab - 认证状态机切页(根部 AnimatedSwitcher 无路由事件):app.dart 的
sessionManager.addListener(_reportAuthStateChange) - 回栈到无名根路由(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 条):
- push 路由报 page_viewed
- push 两页 referrer 链正确,pop 返回补报前一页(didPopNext)
- pop 回无名根路由经 resolveRootPage 补报
- 未在枚举的路由名不上报
- dialog 不上报(PopupRoute 不是 PageRoute)
- PageViewTracker:连续相同页面去重(Tab 重复点选)
- referrer 链跨机制连贯(首页无 referrer)
- 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 三处客户端小修)
- ✅ sessionId 生命周期(P0,会话维度指标前置)
- ✅ eventId 改 UUIDv7(顺手修,保留插入局部性)
- ✅ appVersion/osVersion 动态读取(版本维度可用)
- ✅ page_viewed 路由埋点(M2 新增高频事件,漏斗前置)
- ✅ 生产接线修复(M1 遗留,本波一并关闭)
5.2 未闭环项(排入 M2 第二波或后续迭代)
- shared_preferences 分段持久化队列(13 §3.3 原规范):本波仅顺手加固失败重回队列(一行级),真正的 500 条分段、20 条/段、溢出淘汰最旧段排 M2 第二波(03 §3.2 注意、06 §7.2 工单拆分 1)
- 主壳 Tab/认证切页的 page_viewed 单测:widget_test.dart 主壳冒烟测试未断言 page_viewed 事件(本波集成测试成本高,Tab 切换逻辑已由 PageViewTracker 单测覆盖去重语义)
- 健康档案页面族 RouteSettings 接线:pageName 枚举已预留 pet_list/pet_detail/pet_form/record_form/record_detail,待 M2 健康档案功能落地时挂接(06 §5.2 验收 6 注明「M2 新页待功能落地接线」)
- 真机手测脚本执行:sessionId 生命周期的 30 分钟后台判定需真机/模拟器验证(06 §5.1 验收 6),交付 QA 或开发者手测
6. 质量门禁通过记录
$ 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)
- 通读三份规范(06/03/13)与仓库现状,核对既有 34 测试基线
- 发现 package_info_plus 未在依赖,
flutter pub add package_info_plus新增 - 新建 SessionTracker(WidgetsBindingObserver),注意级联状态处理(首次离开 resumed 才记时)
- 修改 AnalyticsService 三处偏差(eventId v7 / sessionId 注入 / appVersion 可变 / osVersion 动态)
- 修改 app.dart 接线(实例化 analytics + tracker,传给 repository,注册 observer,认证切换监听)
- 新建 page_viewed 四件套(枚举/tracker/observer/接线),主壳 Tab 补点,login_page/fade_route 挂路由名
- 编写 session_tracker_test(5 例)+ analytics_service_test 补强(4 例)+ analytics_route_observer_test(8 例)
flutter test第一轮编译错误:测试 lambda 签名不匹配((name, props)改(name, [props]))flutter analyze第一轮警告:page_view_tracker 的 map literal 空安全操作符误用,改为命令式条件插入- 全绿后
dart format确认无格式变更,提交代码(1 个合并提交),推送 origin/dev
Frontend Developer · 2026-09-07
基线测试 34 全绿 → 收官 51 全绿(+17),flutter analyze 0 问题,已推送。