Files
hjsq_android/analytics_sdk/lib/manager/session_metrics_manager.dart
T
2026-10-08 11:37:25 +07:00

301 lines
8.8 KiB
Dart

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<String> _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<String> _pageKeys = <String>{};
final Set<String> _pagesWithInteraction = <String>{};
final List<String> _behaviorSequence = <String>[];
final List<_PageStayRecord> _activePageStays = <_PageStayRecord>[];
final List<_PageStayRecord> _completedPageStays = <_PageStayRecord>[];
void recordAcceptedEvent(Map<String, dynamic> 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<String, dynamic> event) {
final pageKey = _payloadPageKey(event);
if (pageKey.isNotEmpty) _pageKeys.add(pageKey);
}
String _payloadPageKey(Map<String, dynamic> event) {
return _payloadString(event, 'page_key');
}
String _payloadString(
Map<String, dynamic> 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<Map<String, dynamic>> _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<Map<String, dynamic>> 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<String, dynamic> 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,
};
}
}