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 _effectiveLifecycleKeys = { 'cold_start', 'web_load', 'warm_start', 'foreground', }; static const Set _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 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 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 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() ?? ''; } }