301 lines
8.8 KiB
Dart
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,
|
|
};
|
|
}
|
|
}
|