新增:page_viewed 集中式路由埋点(NavigatorObserver)
CI / flutter-gates (push) Successful in 1m3s

- AnalyticsRouteObserver:didPush/didReplace/didPop 集中捕获,页面零侵入
- AnalyticsPageName 枚举:pageName 编译期锁死(字典 v2 九个初始值 + 客户端现存 create/pet_archive/services/post_detail),字典外路由不上报
- PageViewTracker:page_viewed 单一上报出口,维护 referrer 链(栈底/冷启动首页为 null)与去重(Tab 重复点选)
- 三类非路由曝光手动补点:主壳 Tab 切换(IndexedStack)、认证状态机切页(AnimatedSwitcher)、回栈到无名根路由(resolveRootPage)
- 既有 Navigator.push 挂 RouteSettings.name:login_page(register)、main_shell_page(post_detail);fade_route 签名扩展可选 settings
- M2 健康档案页面(pet_list/pet_detail/pet_form/record_form/record_detail)先留枚举定义,待功能落地接线

测试新增 8 例(analytics_route_observer_test):push 上报/push 两页 referrer 链正确 + pop 补报/根路由 resolveRootPage 补报/枚举外不报/dialog 不报/Tab 去重/referrer 连贯/reportTab。

验收对照(06 号报告 §5.2 六条):1✓ observer 注册、2✓ pageName 枚举含 pet_form、3✓ referrer 链、4✓ 字典外不报、5✓ 单测 push/pop/referrer、6✓ M1 存量四页接全。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 14:50:04 +08:00
parent 4c2f839a01
commit 6fef0db009
7 changed files with 400 additions and 3 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);
}
}
}