import 'dart:convert'; import 'package:crypto/crypto.dart'; /// Session 内自动汇总指标。 /// /// 只记录成功进入 SDK 队列的事件。这里不采集滚动轨迹、键盘输入内容、 /// 长按、双指缩放等高风险/不稳定指标,避免为了汇总事件扩大监听面。 class SessionMetricsManager { static final SessionMetricsManager instance = SessionMetricsManager._internal(); factory SessionMetricsManager() => instance; SessionMetricsManager._internal(); static const int _maxBehaviorSequence = 200; static const int _maxPageStayRecords = 100; static const Set _nonBusinessEvents = { 'sdk_init', 'app_lifecycle', 'network_state_change', 'app_session_summary', 'app_page_view', 'page_click', 'navigation', 'page_lifecycle', }; String _sid = ''; int _eventCountTotal = 0; int _businessEventCount = 0; int _foregroundCount = 0; int _backgroundCountInSid = 0; int _pageViewCount = 0; int _pageClickCount = 0; int _networkChangeCount = 0; String _lastEventName = ''; final Set _pageKeys = {}; final Set _pagesWithInteraction = {}; final List _behaviorSequence = []; final List<_PageStayRecord> _activePageStays = <_PageStayRecord>[]; final List<_PageStayRecord> _completedPageStays = <_PageStayRecord>[]; void recordAcceptedEvent(Map event) { final eventName = event['event']?.toString() ?? ''; if (eventName.isEmpty || eventName == 'app_session_summary') return; final sid = event['sid']?.toString() ?? ''; if (sid != _sid) { reset(sid: sid); } _eventCountTotal += 1; _lastEventName = eventName; _recordBehaviorSequence(eventName, _payloadPageKey(event)); if (!_nonBusinessEvents.contains(eventName)) { _businessEventCount += 1; } if (eventName == 'app_page_view') { _pageViewCount += 1; _recordPageKey(event); } else if (eventName == 'page_click') { _pageClickCount += 1; _recordPageKey(event); final pageKey = _payloadPageKey(event); if (pageKey.isNotEmpty) _pagesWithInteraction.add(pageKey); } else if (eventName == 'app_lifecycle') { final key = _payloadString(event, 'lifecycle_event_key', fallback: event['lifecycle_event_key']); if (key == 'foreground') { _foregroundCount += 1; } else if (key == 'background') { _backgroundCountInSid += 1; } } else if (eventName == 'network_state_change') { _networkChangeCount += 1; } } void recordPageEnter({ required String pageKey, required String pageName, required DateTime enterTime, }) { final normalizedPageKey = pageKey.trim(); if (normalizedPageKey.isEmpty) return; _pageKeys.add(normalizedPageKey); _closeActivePageStays(exitTime: enterTime); _activePageStays.add(_PageStayRecord( pageKey: normalizedPageKey, pageName: pageName, enterTime: enterTime, )); } void recordPageExit({ required String pageKey, required String pageName, required DateTime enterTime, required DateTime exitTime, }) { final normalizedPageKey = pageKey.trim(); if (normalizedPageKey.isEmpty) return; _pageKeys.add(normalizedPageKey); final index = _findActivePageStayIndex(normalizedPageKey, enterTime); if (index < 0) return; final record = _activePageStays.removeAt(index); _completedPageStays.add(record.close( exitTime: exitTime, hasInteraction: _pagesWithInteraction.contains(normalizedPageKey), )); _trimCompletedPageStays(); } SessionMetricsSnapshot snapshot({DateTime? endedAt}) { final now = endedAt ?? DateTime.now(); return SessionMetricsSnapshot( eventCountTotal: _eventCountTotal, businessEventCount: _businessEventCount, foregroundCount: _foregroundCount, backgroundCountInSid: _backgroundCountInSid, pageCount: _pageKeys.length, pageViewCount: _pageViewCount, pageClickCount: _pageClickCount, networkChangeCount: _networkChangeCount, sessionSid: _sid, lastEventName: _lastEventName, pageStayList: _pageStayList(now), behaviorSequenceHash: _behaviorSequenceHash(), ); } void reset({String sid = ''}) { _sid = sid; _eventCountTotal = 0; _businessEventCount = 0; _foregroundCount = 0; _backgroundCountInSid = 0; _pageViewCount = 0; _pageClickCount = 0; _networkChangeCount = 0; _lastEventName = ''; _pageKeys.clear(); _pagesWithInteraction.clear(); _behaviorSequence.clear(); _activePageStays.clear(); _completedPageStays.clear(); } void _recordPageKey(Map event) { final pageKey = _payloadPageKey(event); if (pageKey.isNotEmpty) _pageKeys.add(pageKey); } String _payloadPageKey(Map event) { return _payloadString(event, 'page_key'); } String _payloadString( Map event, String key, { Object? fallback, }) { final payload = event['payload']; if (payload is Map) { final value = payload[key]?.toString().trim() ?? ''; if (value.isNotEmpty) return value; } return fallback?.toString().trim() ?? ''; } void _recordBehaviorSequence(String eventName, String pageKey) { final token = pageKey.isEmpty ? eventName : '$eventName:$pageKey'; _behaviorSequence.add(token); if (_behaviorSequence.length > _maxBehaviorSequence) { _behaviorSequence.removeAt(0); } } String _behaviorSequenceHash() { if (_behaviorSequence.isEmpty) return ''; return sha256.convert(utf8.encode(_behaviorSequence.join('|'))).toString(); } List> _pageStayList(DateTime endedAt) { final all = [ ..._completedPageStays, ..._activePageStays.map((record) => record.close( exitTime: endedAt, hasInteraction: _pagesWithInteraction.contains(record.pageKey), )), ]..sort((a, b) => a.enterTime.compareTo(b.enterTime)); final capped = all.length > _maxPageStayRecords ? all.sublist(all.length - _maxPageStayRecords) : all; return capped.map((record) => record.toJson()).toList(); } void _closeActivePageStays({required DateTime exitTime}) { if (_activePageStays.isEmpty) return; for (final record in _activePageStays) { _completedPageStays.add(record.close( exitTime: exitTime, hasInteraction: _pagesWithInteraction.contains(record.pageKey), )); } _activePageStays.clear(); _trimCompletedPageStays(); } int _findActivePageStayIndex(String pageKey, DateTime enterTime) { for (var i = _activePageStays.length - 1; i >= 0; i--) { final record = _activePageStays[i]; if (record.pageKey == pageKey && record.enterTime == enterTime) return i; } return -1; } void _trimCompletedPageStays() { while (_completedPageStays.length > _maxPageStayRecords) { _completedPageStays.removeAt(0); } } } class SessionMetricsSnapshot { final int eventCountTotal; final int businessEventCount; final int foregroundCount; final int backgroundCountInSid; final int pageCount; final int pageViewCount; final int pageClickCount; final int networkChangeCount; final String sessionSid; final String lastEventName; final List> pageStayList; final String behaviorSequenceHash; const SessionMetricsSnapshot({ required this.eventCountTotal, required this.businessEventCount, required this.foregroundCount, required this.backgroundCountInSid, required this.pageCount, required this.pageViewCount, required this.pageClickCount, required this.networkChangeCount, required this.sessionSid, required this.lastEventName, required this.pageStayList, required this.behaviorSequenceHash, }); } class _PageStayRecord { final String pageKey; final String pageName; final DateTime enterTime; final DateTime? exitTime; final bool hasInteraction; const _PageStayRecord({ required this.pageKey, required this.pageName, required this.enterTime, this.exitTime, this.hasInteraction = false, }); _PageStayRecord close({ required DateTime exitTime, required bool hasInteraction, }) { return _PageStayRecord( pageKey: pageKey, pageName: pageName, enterTime: enterTime, exitTime: exitTime, hasInteraction: hasInteraction, ); } Map toJson() { final endTime = exitTime ?? DateTime.now(); final stayDurationMs = endTime.difference(enterTime).inMilliseconds; return { 'page_key': pageKey, 'page_name': pageName, 'stay_duration_ms': stayDurationMs < 0 ? 0 : stayDurationMs, 'enter_time': enterTime.millisecondsSinceEpoch ~/ 1000, 'has_interaction': hasInteraction ? 1 : 0, }; } }