4d40c38f06
CI / flutter-gates (push) Successful in 2m15s
- 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>
62 lines
2.3 KiB
Dart
62 lines
2.3 KiB
Dart
import 'package:flutter/widgets.dart';
|
||
import 'package:uuid/uuid.dart';
|
||
|
||
/// sessionId 生命周期管理(13 号规范 §3.1、06 号报告 §5.1)。
|
||
///
|
||
/// 语义三条:冷启动生成新 sessionId;`paused → resumed` 间隔超过
|
||
/// [timeout](默认 30 分钟)生成新 sessionId;未超过则沿用原值。
|
||
/// sessionId 不落任何持久化存储——会话本该跨冷启动失效(03 号评估 §3.1,
|
||
/// 纯内存方案,lastActiveAt 不持久化)。
|
||
///
|
||
/// 注意生命周期级联:前台恢复时状态机会依次经过
|
||
/// `paused → hidden → inactive → resumed`,因此只在**离开 resumed 的第一次
|
||
/// 变更**记录退后台时刻,后续级联状态不得覆盖,否则间隔永远趋近于零。
|
||
class SessionTracker with WidgetsBindingObserver {
|
||
SessionTracker({
|
||
this.timeout = const Duration(minutes: 30),
|
||
this.onLeaveForeground,
|
||
this.onEnterForeground,
|
||
DateTime Function()? now,
|
||
}) : _now = now ?? DateTime.now,
|
||
_sessionId = const Uuid().v7();
|
||
|
||
/// 后台超时阈值;构造参数化便于测试注入。
|
||
final Duration timeout;
|
||
|
||
/// 首次离开前台时回调(app 装配层用于触发埋点队列冲刷)。
|
||
final VoidCallback? onLeaveForeground;
|
||
|
||
/// 回到前台时回调(app 装配层用于恢复 30 秒定时冲刷);
|
||
/// 冷启动首个 resumed 不触发(此前未离开过前台)。
|
||
final VoidCallback? onEnterForeground;
|
||
|
||
/// 时钟注入口,测试免真实等待。
|
||
final DateTime Function() _now;
|
||
|
||
String _sessionId;
|
||
DateTime? _leftForegroundAt;
|
||
AppLifecycleState _lastState = AppLifecycleState.resumed;
|
||
|
||
/// 当前会话标识(UUIDv7)。
|
||
String get sessionId => _sessionId;
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed) {
|
||
final leftAt = _leftForegroundAt;
|
||
if (leftAt != null) {
|
||
if (_now().difference(leftAt) > timeout) {
|
||
_sessionId = const Uuid().v7();
|
||
}
|
||
onEnterForeground?.call();
|
||
}
|
||
_leftForegroundAt = null;
|
||
} else if (_lastState == AppLifecycleState.resumed) {
|
||
// 首次离开前台才记时;inactive/hidden/paused 级联不覆盖。
|
||
_leftForegroundAt = _now();
|
||
onLeaveForeground?.call();
|
||
}
|
||
_lastState = state;
|
||
}
|
||
}
|