Files
lixi 4d40c38f06
CI / flutter-gates (push) Successful in 2m15s
新增:埋点队列三项完善——30 秒定时冲刷、失败指数退避、anonymousId 持久化(T3-19)
- 30 秒定时冲刷:AnalyticsService.startPeriodicFlush/stopPeriodicFlush,
  前台期间 Timer.periodic 周期冲刷;SessionTracker 新增 onEnterForeground
  回调,退后台停(并保留既有 flushNow 触发)、回前台恢复,App dispose 收尾。
- 失败退避:网络错误/5xx 后 30s→60s→120s 指数退避封顶 5 分钟,退避窗口
  只挡定时冲刷(flushNow/满 20/冷启动显式触发不受限),上传成功即重置;
  429 改按网络错误同路径保段重试(Retry-After 分支待后端限流落地)。
- anonymousId 持久化:restore 时从 shared_preferences 采用/落盘
  pb.analytics.anonymousId,首次生成后跨冷启动稳定;读取失败降级
  进程内临时 id 不崩溃。
- 测试 272 → 286(+14):fakeAsync 定时/退避 8 个、anonymousId 4 个、
  429 保段 1 个、前后台回调成对 1 个;analyze 0 问题、format 无 diff。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-08 16:12:19 +08:00

105 lines
4.2 KiB
Dart
Raw Permalink 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/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:patbond_flutter/analytics/session_tracker.dart';
void main() {
final uuidV7 = RegExp(
r'^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$',
);
group('SessionTracker', () {
test('冷启动生成 UUIDv7 格式的 sessionId', () {
final tracker = SessionTracker();
expect(tracker.sessionId, matches(uuidV7));
});
test('短后台(≤30 分钟)resume 后沿用原 sessionId', () {
var clock = DateTime.utc(2026, 9, 7, 10);
final tracker = SessionTracker(now: () => clock);
final original = tracker.sessionId;
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
clock = clock.add(const Duration(minutes: 30));
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(tracker.sessionId, original);
});
test('长后台(>30 分钟)resume 后生成新 sessionId', () {
var clock = DateTime.utc(2026, 9, 7, 10);
final tracker = SessionTracker(now: () => clock);
final original = tracker.sessionId;
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
clock = clock.add(const Duration(minutes: 31));
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(tracker.sessionId, isNot(original));
expect(tracker.sessionId, matches(uuidV7));
});
test('真实生命周期级联下退后台时刻不被 inactive 覆盖', () {
// 前台恢复时状态机级联 paused → hidden → inactive → resumed
// 若 inactive 覆盖记时,31 分钟长后台会被误判为 0 分钟。
var clock = DateTime.utc(2026, 9, 7, 10);
final tracker = SessionTracker(now: () => clock);
final original = tracker.sessionId;
tracker.didChangeAppLifecycleState(AppLifecycleState.inactive);
tracker.didChangeAppLifecycleState(AppLifecycleState.hidden);
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
clock = clock.add(const Duration(minutes: 31));
tracker.didChangeAppLifecycleState(AppLifecycleState.hidden);
tracker.didChangeAppLifecycleState(AppLifecycleState.inactive);
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(tracker.sessionId, isNot(original));
});
test('连续多次短后台幂等,仅超阈值才换新', () {
var clock = DateTime.utc(2026, 9, 7, 10);
final tracker = SessionTracker(now: () => clock);
final original = tracker.sessionId;
for (var i = 0; i < 3; i++) {
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
clock = clock.add(const Duration(minutes: 10));
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(tracker.sessionId, original);
}
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
clock = clock.add(const Duration(minutes: 35));
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(tracker.sessionId, isNot(original));
});
test('前后台回调成对触发一次,生命周期级联不重复', () {
var leaveCount = 0;
var enterCount = 0;
final tracker = SessionTracker(
onLeaveForeground: () => leaveCount++,
onEnterForeground: () => enterCount++,
);
// 退后台级联:inactive → hidden → paused 只回调 leave 一次。
tracker.didChangeAppLifecycleState(AppLifecycleState.inactive);
tracker.didChangeAppLifecycleState(AppLifecycleState.hidden);
tracker.didChangeAppLifecycleState(AppLifecycleState.paused);
expect(leaveCount, 1);
expect(enterCount, 0);
// 回前台级联:hidden → inactive → resumed 只回调 enter 一次。
tracker.didChangeAppLifecycleState(AppLifecycleState.hidden);
tracker.didChangeAppLifecycleState(AppLifecycleState.inactive);
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(leaveCount, 1);
expect(enterCount, 1);
// 已在前台重复 resumed(冷启动首个 resumed 同形)不触发 enter。
tracker.didChangeAppLifecycleState(AppLifecycleState.resumed);
expect(enterCount, 1);
});
});
}