Files
patbond-flutter/test/analytics/analytics_route_observer_test.dart
T
lixi f501a959f3
CI / flutter-gates (push) Successful in 1m9s
修复:埋点接线与三处偏差(sessionId/eventId/设备信息)
- 生产接线修复: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>
2026-09-07 14:45:41 +08:00

238 lines
8.2 KiB
Dart
Raw 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.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patbond_flutter/analytics/analytics_page_name.dart';
import 'package:patbond_flutter/analytics/analytics_route_observer.dart';
import 'package:patbond_flutter/analytics/page_view_tracker.dart';
void main() {
group('AnalyticsRouteObserver', () {
testWidgets('push 路由报 page_viewed', (tester) async {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
final observer = AnalyticsRouteObserver(tracker: tracker);
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(
name: AnalyticsPageName.login.pageName,
),
builder: (_) => const Placeholder(),
),
),
child: const Text('Go'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(events, hasLength(1));
expect(events.first['event'], 'page_viewed');
expect(events.first['pageName'], 'login');
});
testWidgets('push 两页 referrer 链正确,pop 返回补报前一页', (tester) async {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
final observer = AnalyticsRouteObserver(tracker: tracker);
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(
name: AnalyticsPageName.login.pageName,
),
builder: (loginContext) => ElevatedButton(
onPressed: () => Navigator.of(loginContext).push(
MaterialPageRoute<void>(
settings: RouteSettings(
name: AnalyticsPageName.register.pageName,
),
builder: (registerContext) => ElevatedButton(
onPressed: () => Navigator.of(registerContext).pop(),
child: const Text('Back'),
),
),
),
child: const Text('Next'),
),
),
),
child: const Text('Go'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.tap(find.text('Next'));
await tester.pumpAndSettle();
// push 两页:两条事件,referrer 链正确(06 §5.2 验收 5)。
expect(events.map((e) => e['pageName']), ['login', 'register']);
expect(events[0].containsKey('referrer'), isFalse);
expect(events[1]['referrer'], 'login');
// pop 返回:didPop 补报重新曝光的前一页,referrer 为弹出页。
await tester.tap(find.text('Back'));
await tester.pumpAndSettle();
expect(events, hasLength(3));
expect(events[2]['pageName'], 'login');
expect(events[2]['referrer'], 'register');
});
testWidgets('pop 回无名根路由经 resolveRootPage 补报', (tester) async {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
final observer = AnalyticsRouteObserver(
tracker: tracker,
resolveRootPage: () => AnalyticsPageName.home,
);
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
settings: RouteSettings(
name: AnalyticsPageName.profile.pageName,
),
builder: (innerContext) => ElevatedButton(
onPressed: () => Navigator.of(innerContext).pop(),
child: const Text('Back'),
),
),
),
child: const Text('Go'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.tap(find.text('Back'));
await tester.pumpAndSettle();
expect(events.map((e) => e['pageName']), ['profile', 'home']);
expect(events[1]['referrer'], 'profile');
});
testWidgets('未在枚举的路由名不上报', (tester) async {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
final observer = AnalyticsRouteObserver(tracker: tracker);
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(
settings: const RouteSettings(name: '/debug/unknown'),
builder: (_) => const Placeholder(),
),
),
child: const Text('Go'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(events, isEmpty);
});
testWidgets('dialog 不上报(PopupRoute 不是 PageRoute', (tester) async {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
final observer = AnalyticsRouteObserver(tracker: tracker);
await tester.pumpWidget(
MaterialApp(
navigatorObservers: [observer],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => showDialog<void>(
context: context,
builder: (_) => const AlertDialog(title: Text('Dialog')),
),
child: const Text('Show'),
),
),
),
);
await tester.tap(find.text('Show'));
await tester.pumpAndSettle();
expect(events, isEmpty);
});
});
group('PageViewTracker', () {
test('连续相同页面去重(Tab 重复点选)', () {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
tracker.report(AnalyticsPageName.home);
tracker.report(AnalyticsPageName.home);
tracker.report(AnalyticsPageName.profile);
tracker.report(AnalyticsPageName.home);
expect(events, hasLength(3));
expect(events.map((e) => e['pageName']), ['home', 'profile', 'home']);
});
test('referrer 链跨机制连贯(首页无 referrer', () {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
tracker.report(AnalyticsPageName.home);
tracker.report(AnalyticsPageName.login);
tracker.report(AnalyticsPageName.profile);
expect(events[0].containsKey('referrer'), isFalse);
expect(events[1]['referrer'], 'home');
expect(events[2]['referrer'], 'login');
});
test('reportTab 记录当前 Tab', () {
final events = <Map<String, dynamic>>[];
final tracker = PageViewTracker((name, [props]) async {
events.add({'event': name, ...?props});
});
tracker.reportTab(AnalyticsPageName.petArchive);
expect(tracker.currentTab, AnalyticsPageName.petArchive);
expect(events.single['pageName'], 'pet_archive');
});
});
}