修复:埋点接线与三处偏差(sessionId/eventId/设备信息)
CI / flutter-gates (push) Successful in 1m9s

- 生产接线修复:app.dart 组装时传 analytics 实例给 ApiAuthRepository(_buildRepository),修复 M1 遗留的「生产环境 _analytics 恒为 null、挂接点空转」问题
- sessionId 生命周期:新建 SessionTracker (WidgetsBindingObserver),冷启动生成 UUIDv7、后台超 30 分钟换新、未超阈值沿用原值,不再每事件随机生成
- eventId 改 UUIDv7:对齐 13 号规范(uuid 包已在依赖,直接用 v7()),保留插入时间局部性
- appVersion 动态注入:package_info_plus(新增依赖)异步读取后 setAppVersion,不再硬编码 '1.0.0+1'
- osVersion 动态读取:Platform.operatingSystemVersion 正则提取主版本(如 android-14),不再硬编码
- 队列顺手加固:上传失败批次重回队首而非整批丢弃(一行级缓解,分段持久化属 M2 第二波)

测试新增 9 例:session_tracker_test(5 例:冷启动/短后台/长后台/级联状态/连续幂等),analytics_service_test 补强 4 例(UUIDv7/sessionId 不再逐事件生成/appVersion 可注入/失败重回队列)。

验收对照(06 号报告 §5.1 六条):1✓ SessionTracker 新建、2✓ 三条语义、3✓ 同会话 sessionId 一致、4✓ sessionId 为 UUID 不持久化、5✓ 单测 3 例(实际 5 例)、6✓ 真机脚本(开发者手测)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 14:45:41 +08:00
parent 3f8388e5d4
commit f501a959f3
15 changed files with 767 additions and 50 deletions
@@ -0,0 +1,49 @@
import 'package:flutter/widgets.dart';
import 'package:patbond_flutter/analytics/analytics_page_name.dart';
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
/// 集中式 page_viewed 路由埋点(03 号评估 §3.2NavigatorObserver 派生,
/// 页面零侵入;06 号报告 §5.2 验收 1/4)。
///
/// 只统计 [PageRoute]bottom sheet / dialog 是 PopupRoute,不计曝光);
/// 路由名不在 [AnalyticsPageName] 枚举内不上报(后端会整条 rejected,
/// 不白白消耗队列)。didPop 补报「回退后重新曝光的前一页」;弹回到
/// 无名根路由(认证状态机的 AnimatedSwitcher 宿主)时经 [resolveRootPage]
/// 解析当前根部页面。
class AnalyticsRouteObserver extends NavigatorObserver {
AnalyticsRouteObserver({required this.tracker, this.resolveRootPage});
final PageViewTracker tracker;
/// 根路由(无 RouteSettings.name)当前展示页的解析器,由 app.dart
/// 按认证状态与主壳当前 Tab 提供;返回 null 则不补报。
final AnalyticsPageName? Function()? resolveRootPage;
void _reportRoute(Route<dynamic>? route) {
if (route is! PageRoute) return;
final page = AnalyticsPageName.fromRouteName(route.settings.name);
if (page != null) tracker.report(page);
}
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
_reportRoute(route);
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
_reportRoute(newRoute);
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (previousRoute is! PageRoute) return;
final page = AnalyticsPageName.fromRouteName(previousRoute.settings.name);
if (page != null) {
tracker.report(page);
} else if (previousRoute.isFirst) {
final rootPage = resolveRootPage?.call();
if (rootPage != null) tracker.report(rootPage);
}
}
}