Initial commit: 91dj android client dev_1.0.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
谢宇宁
2026-10-08 11:37:25 +07:00
co-authored by Claude Opus 5.5
commit 97dd6a739a
1262 changed files with 119813 additions and 0 deletions
@@ -0,0 +1,316 @@
import 'package:flutter/foundation.dart';
import 'package:analytics_sdk/utils/logger.dart';
import 'package:analytics_sdk/utils/uuid_util.dart';
/// 会话管理器:管理会话ID(sid)的生成和更新
///
/// 会话定义:用户在一次连续、自然的产品使用过程中产生的一组行为集合
///
/// **sid 说明**:sid 由客户端 SDK 自动生成,每个事件均会携带。
/// 服务端上报事件时,需通过 [AnalyticsSdk.getParams] 获取当前 sid 并一同回传,
/// 以便服务端事件与客户端事件归属同一会话。
///
/// 会话中断阈值:30 分钟无任何有效行为,生成新会话ID
///
/// APP产品定义方案,任意满足一条就重新生成会话ID:
/// 1. App 冷启动(进程首次启动)
/// 2. App 从后台切换前台,且后台停留 > 30 分钟
/// 3. 上一次行为距离当前 > 30 分钟
class SessionManager {
static final SessionManager instance = SessionManager._internal();
factory SessionManager() => instance;
SessionManager._internal();
/// 当前会话ID
String? _currentSessionId;
/// 会话创建时间
DateTime? _sessionStartTime;
/// 最后一次行为时间
DateTime? _lastActivityTime;
/// 应用进入后台的时间
DateTime? _backgroundTime;
/// 当前正在通知结束的旧 session 逻辑结束时间。
DateTime? _notifyingSessionEndTime;
/// 会话中断阈值(30分钟)
static const Duration sessionTimeout = Duration(minutes: 30);
static const Set<String> _effectiveLifecycleKeys = {
'cold_start',
'web_load',
'warm_start',
'foreground',
};
static const Set<String> _nonBusinessActivityEvents = {
'sdk_init',
'network_state_change',
'app_session_summary',
'navigation',
'page_lifecycle',
};
/// 是否已初始化(用于判断冷启动)
bool _isInitialized = false;
/// 新 session 创建时的回调(由外部注入,避免直接依赖 AdImpressionManager)
/// 通常由 AnalyticsSdk.init() 注入:`() => AdImpressionManager.instance.clear()`
void Function()? onNewSession;
/// 旧 session 即将结束时的回调。
///
/// 在新 sid 生成前调用,使调用方仍能用旧 sid 补发 app_session_summary。
void Function(String sessionEndReason)? onSessionEnding;
/// 获取当前会话ID
/// 如果当前没有会话或会话已过期,会自动创建新会话
String getSessionId() {
final now = DateTime.now();
// 检查是否需要生成新会话
// 注意:这里只检查和创建会话,不更新 _lastActivityTime
// 调用方应通过 recordActivity() 显式记录行为时间,避免非行为调用(如 debug 面板)误延长会话
if (_shouldCreateNewSession(now)) {
_createNewSession(now);
}
// 安全检查:确保会话ID不为null(理论上不会发生,但作为保护措施)
if (_currentSessionId == null) {
Logger.analyticsSdk('警告:会话ID为null,强制创建新会话', level: LogLevel.warn);
_createNewSession(now);
}
return _currentSessionId!;
}
/// 检查是否需要创建新会话
bool _shouldCreateNewSession(DateTime now) {
// 1. 如果还没有会话ID,需要创建
if (_currentSessionId == null) {
return true;
}
// 2. 如果还未初始化,说明是冷启动,需要创建新会话
if (!_isInitialized) {
return true;
}
// 3. 如果从后台恢复,且后台停留时间 > 30 分钟
// 注意:这里只在超时时清空 _backgroundTime(并返回 true 触发新会话)。
// 非超时分支不清空,保留给 onAppForeground() 统一处理,
// 避免后台期间有代码调用 getSessionId()(如推送通知触发 track())时
// 提前清空导致 onAppForeground() 无法正确判断后台时长。
if (_backgroundTime != null) {
final backgroundDuration = now.difference(_backgroundTime!);
if (backgroundDuration > sessionTimeout) {
Logger.analyticsSdk(
'后台停留时间 ${backgroundDuration.inMinutes} 分钟,超过阈值,创建新会话');
_backgroundTime = null;
return true;
}
// 未超时:不清空 _backgroundTime,由 onAppForeground() 负责清空
}
// 4. 如果上一次行为距离当前 > 30 分钟
if (_lastActivityTime != null) {
final timeSinceLastActivity = now.difference(_lastActivityTime!);
if (timeSinceLastActivity > sessionTimeout) {
Logger.analyticsSdk(
'距离上次行为 ${timeSinceLastActivity.inMinutes} 分钟,超过阈值,创建新会话');
return true;
}
}
return false;
}
/// 创建新会话
void _createNewSession(DateTime now) {
try {
_notifySessionEndingIfNeeded('idle');
_currentSessionId = generateUuidV4().replaceAll("-", "");
_sessionStartTime = now;
_lastActivityTime = now;
_isInitialized = true;
// 新 session 开始时通知外部清理广告去重状态
onNewSession?.call();
Logger.analyticsSdk('创建新会话ID: $_currentSessionId');
} catch (e) {
// UUID 生成失败时使用时间戳作为备选方案
final timestamp = now.millisecondsSinceEpoch;
_currentSessionId = 'session_${timestamp}_${now.microsecondsSinceEpoch}';
_sessionStartTime = now;
_lastActivityTime = now;
_isInitialized = true;
onNewSession?.call();
Logger.analyticsSdk('UUID 生成失败,使用时间戳创建会话ID: $_currentSessionId');
}
}
/// 初始化会话管理器
/// 在 SDK 初始化时调用,标记为冷启动
void initialize() {
if (!_isInitialized) {
final now = DateTime.now();
_createNewSession(now);
Logger.analyticsSdk('会话管理器初始化(冷启动),创建新会话');
}
}
/// 应用进入后台
/// 在应用生命周期变为后台时调用
void onAppBackground() {
_backgroundTime = DateTime.now();
Logger.analyticsSdk('应用进入后台,记录时间: $_backgroundTime');
}
/// 应用从后台恢复
/// 在应用生命周期变为前台时调用
void onAppForeground() {
final now = DateTime.now();
// 如果从后台恢复,检查是否需要创建新会话
if (_backgroundTime != null) {
final backgroundDuration = now.difference(_backgroundTime!);
if (backgroundDuration > sessionTimeout) {
Logger.analyticsSdk(
'从后台恢复,后台停留 ${backgroundDuration.inMinutes} 分钟,超过阈值,创建新会话');
_createNewSession(now);
} else {
Logger.analyticsSdk(
'从后台恢复,后台停留 ${backgroundDuration.inMinutes} 分钟,继续使用当前会话');
// 更新最后行为时间,表示用户重新活跃
_lastActivityTime = now;
}
_backgroundTime = null; // 清空后台时间
} else {
// 如果没有后台时间记录,直接更新最后行为时间
_lastActivityTime = now;
}
}
/// 记录用户行为
/// 每次生成事件时调用,用于更新最后行为时间
void recordActivity() {
_lastActivityTime = DateTime.now();
}
/// 仅在成功进入 SDK 队列且属于有效行为时更新最后行为时间。
void recordActivityForEvent(Map<String, dynamic> event, {DateTime? at}) {
if (!_isEffectiveActivityEvent(event)) return;
_lastActivityTime = at ?? DateTime.now();
}
/// 获取当前会话ID(不触发新会话创建)
String? get currentSessionId => _currentSessionId;
/// 获取会话开始时间
DateTime? get sessionStartTime => _sessionStartTime;
/// 获取最后行为时间
DateTime? get lastActivityTime => _lastActivityTime;
/// 当前 session 结束回调内可读取的逻辑结束时间。
DateTime? get sessionEndTimeForNotification => _notifyingSessionEndTime;
/// 仅供测试使用:获取后台时间(验证 _shouldCreateNewSession 不提前清空)
@visibleForTesting
DateTime? get backgroundTimeForTest => _backgroundTime;
@visibleForTesting
void setBackgroundTimeForTest(DateTime? value) {
_backgroundTime = value;
}
@visibleForTesting
DateTime? get sessionEndTimeForNotificationForTest =>
_notifyingSessionEndTime;
@visibleForTesting
void setLastActivityTimeForTest(DateTime? value) {
_lastActivityTime = value;
}
@visibleForTesting
void setSessionStartTimeForTest(DateTime? value) {
_sessionStartTime = value;
}
/// QA 调试用:把后台停留时间虚拟为指定时长,便于手工触发 idle summary。
///
/// Release 包中不生效,避免接入方误用影响真实 session 规则。
bool debugSetBackgroundElapsedForQa(Duration elapsed) {
if (kReleaseMode) return false;
if (elapsed <= Duration.zero) return false;
final now = DateTime.now();
_backgroundTime = now.subtract(elapsed);
_lastActivityTime = now.subtract(elapsed);
return true;
}
/// 手动重置会话(用于测试或特殊场景)
void reset() {
_currentSessionId = null;
_sessionStartTime = null;
_lastActivityTime = null;
_backgroundTime = null;
_notifyingSessionEndTime = null;
_isInitialized = false;
Logger.analyticsSdk('会话已重置');
}
void _notifySessionEndingIfNeeded(String reason) {
if (_currentSessionId == null || !_isInitialized) return;
final previousEndTime = _notifyingSessionEndTime;
_notifyingSessionEndTime = _logicalSessionEndTime(
reason: reason,
now: DateTime.now(),
);
try {
onSessionEnding?.call(reason);
} catch (e) {
Logger.analyticsSdk('旧 session 结束回调执行失败: $e', level: LogLevel.warn);
} finally {
_notifyingSessionEndTime = previousEndTime;
}
}
DateTime _logicalSessionEndTime({
required String reason,
required DateTime now,
}) {
if (reason != 'idle') return now;
final sessionStart = _sessionStartTime ?? now;
final rawLastActivity = _lastActivityTime ?? sessionStart;
final lastActivity =
rawLastActivity.isBefore(sessionStart) ? sessionStart : rawLastActivity;
final logicalEnd = lastActivity.add(sessionTimeout);
return logicalEnd.isAfter(now) ? now : logicalEnd;
}
bool _isEffectiveActivityEvent(Map<String, dynamic> event) {
final eventName = event['event']?.toString() ?? '';
if (eventName.isEmpty) return false;
if (eventName == 'app_lifecycle') {
final key = _lifecycleEventKey(event);
return _effectiveLifecycleKeys.contains(key);
}
if (_nonBusinessActivityEvents.contains(eventName)) return false;
return true;
}
String _lifecycleEventKey(Map<String, dynamic> event) {
final payload = event['payload'];
if (payload is Map) {
final value = payload['lifecycle_event_key']?.toString().trim() ?? '';
if (value.isNotEmpty) return value;
}
return event['lifecycle_event_key']?.toString().trim() ?? '';
}
}