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

1590 lines
56 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:async';
import 'dart:convert';
export 'widget/analytics_debug_banner.dart';
export 'entity/analytics_tab.dart';
export 'entity/ad_sdk_events.dart';
export 'entity/app_page_item_event.dart';
export 'entity/app_lifecycle_event.dart';
export 'entity/app_session_summary_event.dart';
export 'entity/network_state_change_event.dart';
export 'extension/tab_analytics_extension.dart';
export 'enum/page_route_enum.dart';
import 'package:analytics_sdk/config/api_config.dart';
import 'package:analytics_sdk/config/sdk_config.dart';
import 'package:analytics_sdk/const/event_type.dart';
import 'package:analytics_sdk/entity/ad_sdk_events.dart';
import 'package:analytics_sdk/entity/ad_impression_event.dart';
import 'package:analytics_sdk/entity/app_lifecycle_event.dart';
import 'package:analytics_sdk/entity/app_session_summary_event.dart';
import 'package:analytics_sdk/manager/sdk_context.dart';
import 'package:analytics_sdk/enum/user_type_enum.dart';
import 'package:analytics_sdk/entity/sdk_init_event.dart';
import 'package:analytics_sdk/manager/ad_impression_manager.dart';
import 'package:analytics_sdk/manager/event_processor.dart';
import 'package:analytics_sdk/manager/user_manager.dart';
import 'package:analytics_sdk/manager/domain_manager.dart';
import 'package:analytics_sdk/manager/event_type_config_manager.dart';
import 'package:analytics_sdk/manager/session_manager.dart';
import 'package:analytics_sdk/manager/sdk_activation_store.dart';
import 'package:analytics_sdk/manager/sdk_init_payload_builder.dart';
import 'package:analytics_sdk/manager/sdk_init_platform_fields.dart';
import 'package:analytics_sdk/manager/session_behavior_manager.dart';
import 'package:analytics_sdk/manager/session_behavior_monitor.dart';
import 'package:analytics_sdk/manager/session_metrics_manager.dart';
import 'package:analytics_sdk/manager/session_sensor_sampler.dart';
import 'package:analytics_sdk/observer/app_lifecycle_observer.dart';
import 'package:analytics_sdk/observer/app_lifecycle_web_unload_stub.dart'
if (dart.library.html) 'package:analytics_sdk/observer/app_lifecycle_web_unload_web.dart';
import 'package:analytics_sdk/entity/app_page_view_event.dart';
import 'package:analytics_sdk/entity/navigation_event.dart';
import 'package:analytics_sdk/manager/page_name_manager.dart';
import 'package:analytics_sdk/manager/page_observer_registration_guard.dart';
import 'package:analytics_sdk/observer/page_lifecycle_observer.dart';
import 'package:analytics_sdk/utils/aes_gcm_util.dart';
import 'package:analytics_sdk/utils/device_id_util.dart';
import 'package:analytics_sdk/utils/device_fingerprint_util.dart';
import 'package:analytics_sdk/utils/device_info_util.dart';
import 'package:analytics_sdk/utils/environment_info_util.dart';
import 'package:analytics_sdk/utils/event_validator.dart';
import 'package:analytics_sdk/utils/logger.dart';
import 'package:analytics_sdk/utils/widget_bridge.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class AnalyticsSdk {
static final AnalyticsSdk instance = AnalyticsSdk._internal();
factory AnalyticsSdk() => instance;
AnalyticsSdk._internal();
final EventProcessor _eventProcessor = EventProcessor();
/// 单调递增的导航序号,用于快速连续切换时的去重。
/// 每次 [trackNavigation] 调用自增;回调触发时序号不匹配则丢弃。
/// 不在 init() 重置:计数器单调递增,旧回调 generation 永远小于新调用,天然隔离。
int _navigationGeneration = 0;
/// 最近一次成功上报的导航 pageKey,用于拦截同 Tab 重复点击。
String _lastReportedNavigationKey = '';
PageLifecycleObserver? _pageObserver;
PageLifecycleObserver? _sessionObserver;
String Function(String? actualRoute)? _realRouteResolver;
final PageObserverRegistrationGuard _pageObserverRegistrationGuard =
PageObserverRegistrationGuard();
PageLifecycleObserver _createPageObserver({
required bool autoTrackPageViews,
}) {
return PageLifecycleObserver(
track: track,
getUserType: () => AnalyticsSdk.userTypeProvider(),
onPageExit: (pageKey) => AdImpressionManager.instance.clearPage(pageKey),
resolveRealRoute: _realRouteResolver,
autoTrackPageViews: autoTrackPageViews,
);
}
static bool _enableDebugBanner = false;
/// 是否启用调试条(供 AnalyticsDebugBanner 读取)
static bool get enableDebugBanner => _enableDebugBanner;
/// 防止 init() 并发调用(异步挂起期间重复调用时直接跳过)
bool _isInitializing = false;
/// 当前进程内 sdk_init 只上报一次,匹配冷启动/Web 首次加载语义。
bool _sdkInitReported = false;
String _lastSessionSummarySid = '';
AppLifecycleWebUnloadDisposer? _webLifecycleUnloadDisposer;
// ==================== 初始化 ====================
/// 初始化 SDK。
///
/// 本方法永远不会抛出异常,即使初始化失败也不影响应用运行。
///
/// **参数说明**
///
/// - [appId]:应用 ID(必填)
/// - [channel]:渠道标识,可在初始化后通过 [setChannel] 更新
/// - [uid]:用户 ID,未登录时可传空,登录后通过 [setUserIdAndType] 或 [setUid] 更新
/// - [deviceId]:已废弃,SDK 不再信任外部传入 device_id,会内部生成
/// - [appVersion]:应用版本号,格式 `1.0.0`,由接入方传入,可不传
/// - [encryptedConfig]:加密配置字符串(Base64 编码 AES 密文),解密后为
/// `{"domainList":["https://..."],"eventList":["event_type",...]}` 格式,可选
///
/// device、deviceBrand、deviceModel、userAgent、systemName、systemVersion
/// 由 SDK 内部通过 device_info_plus 自动检测,无需外部传入。
Future<void> init({
required String appId,
required String? encryptedConfig,
String? channel,
String? uid,
@Deprecated('device_id 由 SDK 内部生成,外部传入值会被忽略') String? deviceId,
String? appVersion,
bool enableDebugBanner = false,
}) async {
if (_isInitializing) {
Logger.analyticsSdk('init() 正在进行中,跳过重复调用');
return;
}
final initStartedAt = DateTime.now();
_isInitializing = true;
_pageObserverRegistrationGuard.reset();
try {
_enableDebugBanner = !kReleaseMode && enableDebugBanner;
WidgetBridge.register(
track: track,
getUserType: userTypeProvider,
enableDebugBanner: _enableDebugBanner,
getDebugSteps: getDebugSteps,
);
_eventProcessor.stopAutoUploadTimer();
_eventProcessor.resetState();
_lastSessionSummarySid = '';
SessionMetricsManager.instance.reset();
SessionBehaviorManager.instance.reset();
SessionSensorSampler.instance.reset();
SessionBehaviorMonitor.instance.start();
await DeviceInfoUtil.initialize();
await EnvironmentInfoUtil.initialize();
final device = DeviceInfoUtil.deviceType;
final deviceBrand = DeviceInfoUtil.deviceBrand;
final deviceModel = DeviceInfoUtil.deviceModel;
final userAgent = DeviceInfoUtil.userAgent;
final systemName = DeviceInfoUtil.systemName;
final systemVersion = DeviceInfoUtil.systemVersion;
final sdkDeviceId = await DeviceIdUtil.getDeviceId();
SdkContext.configure(
appId: appId,
channel: channel,
uid: uid,
appVersion: appVersion,
device: device,
deviceId: sdkDeviceId,
deviceBrand: deviceBrand,
deviceModel: deviceModel,
userAgent: userAgent,
systemName: systemName,
systemVersion: systemVersion,
environmentFields: EnvironmentInfoUtil.fields,
);
EnvironmentInfoUtil.startMonitoring(track: track);
Logger.analyticsSdk('响应解密已启用(使用内置密钥)');
// re-init 时先重置会话,确保新一轮初始化生成新 session
SessionManager.instance.reset();
_lastReportedNavigationKey = '';
// 注入新 session 回调:新 session 开始时清空广告去重状态
SessionManager.instance.onNewSession = () {
AdImpressionManager.instance.clear();
SessionMetricsManager.instance.reset(
sid: SessionManager.instance.currentSessionId ?? '',
);
final pageLifecycleObserver = _pageObserver ?? _sessionObserver;
final sessionStartedAt = SessionManager.instance.sessionStartTime;
if (pageLifecycleObserver != null && sessionStartedAt != null) {
pageLifecycleObserver.startCurrentPageStayForNewSession(
sessionStartedAt,
);
}
SessionBehaviorManager.instance.reset(
sid: SessionManager.instance.currentSessionId ?? '',
);
SessionSensorSampler.instance.reset(
sid: SessionManager.instance.currentSessionId ?? '',
);
};
SessionManager.instance.onSessionEnding =
(reason) => _trackSessionSummary(sessionEndReason: reason);
SessionManager.instance.initialize();
// 注入 SID 提供者,避免 entity 层直接依赖 SessionManager
SdkContext.sidProvider = () {
return SessionManager.instance.getSessionId();
};
AppLifecycleObserver.instance.initialize(track: track);
// 初始化设备指纹
_prepareDeviceFingerprint();
await _applyEncryptedConfig(
encryptedConfig: encryptedConfig,
source: 'init',
);
try {
await _eventProcessor.initPersistence();
} catch (e) {
Logger.analyticsSdk('持久化初始化失败,继续运行: $e');
}
try {
await _eventProcessor.loadCachedEvents();
} catch (e) {
Logger.analyticsSdk('缓存加载失败,继续运行: $e');
}
await _trackSdkInit(initStartedAt: initStartedAt);
_eventProcessor.startAutoUploadTimer();
_schedulePageObserverRegistrationCheck();
} catch (e) {
Logger.analyticsSdk('初始化异常: $e');
} finally {
_isInitializing = false;
}
}
void _schedulePageObserverRegistrationCheck() {
try {
WidgetsBinding.instance.addPostFrameCallback((_) {
_pageObserverRegistrationGuard.check(
pageObserverAttached: _pageObserver?.navigator != null,
sessionObserverAttached: _sessionObserver?.navigator != null,
);
});
// init() 发生在首帧之后时,主动请求一帧,避免接入检查长期不执行。
WidgetsBinding.instance.ensureVisualUpdate();
} catch (e) {
Logger.analyticsSdk('页面 Observer 接入检查注册失败: $e');
}
}
// ==================== UID / Channel 设置 ====================
/// 更新用户 ID 和用户类型(登录、切换账号时调用)。
///
/// [userId] 为空时表示未登录状态。
/// 本方法永远不会抛出异常。
static void setUserIdAndType(
{String userId = '', UserTypeEnum? userTypeEnum}) {
try {
SdkContext.setUid(userId);
UserManager.instance.updateUserType(userTypeEnum == null
? UserTypeEnum.normal.label
: userTypeEnum.label);
Logger.analyticsSdk('用户ID已更新为: $userId');
} catch (e) {
Logger.analyticsSdk('设置用户信息失败: $e');
}
}
/// 单独更新用户 ID(不影响用户类型)。
///
/// 适用于已知 userType 无变化、只需更新 uid 的场景(如静默登录)。
/// 本方法永远不会抛出异常。
static void setUid(String uid) {
try {
SdkContext.setUid(uid);
Logger.analyticsSdk('uid 已更新为: $uid');
} catch (e) {
Logger.analyticsSdk('setUid 失败: $e');
}
}
/// 更新渠道标识(渠道变更时调用,后续事件自动带上最新值)。
///
/// 本方法永远不会抛出异常。
static void setChannel(String channel) {
try {
SdkContext.setChannel(channel);
Logger.analyticsSdk('channel 已更新为: $channel');
} catch (e) {
Logger.analyticsSdk('setChannel 失败: $e');
}
}
/// 登出:清空 uid 和 userType。
///
/// 本方法永远不会抛出异常。
static void logoutUser() {
try {
SdkContext.setUid('');
UserManager.instance.logout();
Logger.analyticsSdk('用户已登出');
} catch (e) {
Logger.analyticsSdk('用户登出失败: $e');
}
}
// ==================== 设备公共字段 ====================
/// 获取当前设备公共字段(供产品方在服务端上报事件时复用)。
///
/// 返回字段:sid、device、device_id、device_brand、device_model、
/// user_agent、system_name、system_version、device_fingerprint、fp_version。
/// 未配置的字段返回空字符串。
///
/// `sid` 为客户端 SDK 自动生成的会话 ID;服务端上报事件时应将其一并回传。
///
/// 本方法永远不会抛出异常。
static Map<String, String> getDeviceCommonFields() {
try {
return SdkContext.getDeviceCommonFields();
} catch (e) {
Logger.analyticsSdk('getDeviceCommonFields 失败: $e');
return {};
}
}
/// 获取 SDK 当前全部公共参数,或按 [keys] 指定只返回部分参数。
///
/// - 不传 [keys](或传 null):返回全部参数
/// - 传入 key 列表:只返回命中的 key,不存在的 key 会被忽略
///
/// 可用 key:`app_id` `channel` `uid` `user_type` `sid`
/// `device` `device_id` `device_brand` `device_model`
/// `user_agent` `system_name` `system_version`
/// `device_fingerprint` `fp_version` `sdk_version` `app_version`
///
/// `sid` 为客户端 SDK 自动生成的会话ID,服务端上报事件时需将其一并回传,
/// 以确保服务端事件与客户端事件归属同一会话。
///
/// 本方法永远不会抛出异常。
static Map<String, dynamic> getParams([List<String>? keys]) {
try {
final all = <String, dynamic>{
'app_id': SdkContext.appId ?? '',
'channel': SdkContext.channel ?? '',
'uid': SdkContext.uid ?? '',
'user_type': UserManager.instance.userType,
'sid': SessionManager.instance.currentSessionId ?? '',
'sdk_version': SdkContext.kSdkVersion,
'app_version': SdkContext.appVersion ?? SdkContext.kDefaultAppVersion,
...SdkContext.getDeviceCommonFields(),
};
if (keys == null || keys.isEmpty) return all;
return {
for (final k in keys)
if (all.containsKey(k)) k: all[k]
};
} catch (e) {
Logger.analyticsSdk('getParams 失败: $e');
return {};
}
}
// ==================== 上报 ====================
/// 自动页面曝光模式的生命周期观察者。
///
/// 注册后既记录 Session 页面停留,也自动追踪所有命名路由的
/// [AppPageViewEvent]。与 [sessionObserver] 必须二选一,不能同时注册。
///
/// ```dart
/// MaterialApp(
/// navigatorObservers: [AnalyticsSdk.instance.pageObserver],
/// )
/// ```
NavigatorObserver get pageObserver =>
_pageObserver ??= _createPageObserver(autoTrackPageViews: true);
/// 手动页面曝光模式的生命周期观察者。
///
/// 只记录 Session 页面停留,不自动上报 [AppPageViewEvent]。如果业务已手动
/// 上报页面曝光,应在 [MaterialApp.navigatorObservers] 中注册此 observer。
/// 与 [pageObserver] 必须二选一,不能同时注册。
///
/// ```dart
/// MaterialApp(
/// navigatorObservers: [AnalyticsSdk.instance.sessionObserver],
/// )
/// ```
NavigatorObserver get sessionObserver =>
_sessionObserver ??= _createPageObserver(autoTrackPageViews: false);
/// 注入路由解析回调,将实际路径(如 `/video/123`)转换为模板路径(`/video/:id`)。
///
/// 与 [PageNameMapper.addMappings] 配合使用,实现带参数路由的稳定 pageKey 归因。
/// GoRouter 场景通常不需要注入(`settings.name` 即为完整路径);
/// GetX 等路由框架可能需要注入以获取模板路径。
///
/// 传入 `null` 可清除已注入的回调,恢复默认行为(直接使用 `settings.name`)。
void setRealRouteResolver(String Function(String? actualRoute)? resolver) {
_realRouteResolver = resolver;
_pageObserver?.setRealRouteResolver(resolver);
_sessionObserver?.setRealRouteResolver(resolver);
}
/// 手动上报导航事件,在用户点击导航菜单时调用。
///
/// **适用场景**:底部 Tab、顶部 Tab、侧边抽屉等导航菜单点击。
/// 内部同时调用 [PageLifecycleObserver.recordNavigation],更新全局 currentPageKey
/// 及栈顶 effectivePageKey,确保后续 push 的 referrer 和 page_click 归属均正确。
///
/// **已接入 [BottomNavigationBar.withAnalytics] / [TabBar.withAnalytics] 时无需调用**,
/// 包装器内部已处理;仅在完全手动管理导航时使用。
///
/// 同时上报 [AppPageViewEvent] 和 [NavigationEvent],通过
/// [WidgetsBinding.addPostFrameCallback] 在渲染完成后触发;
/// [pageLoadTime] 反映从调用到渲染完成的实际耗时。
/// 快速连续切换时只上报最后一次目标,中间状态自动丢弃。
///
/// - [pageKey]:导航目标的唯一标识,建议使用 [AppPageEnum](如 `AppPageEnum.video.key`),
/// 也可传入自定义字符串;与 app_page_view 的 page_key 保持一致
/// - [pageName]:导航目标的展示名称,建议使用 [AppPageEnum](如 `AppPageEnum.video.name`)
///
/// 本方法永远不会抛出异常。
void trackNavigation({
required String pageKey,
required String pageName,
}) {
try {
PageNameMapper.addMapping(pageKey, pageName);
final referrerKey = PageLifecycleObserver.currentPageKey;
final referrerName = PageNameMapper.getPageName(referrerKey);
final tapTime = DateTime.now();
PageLifecycleObserver.recordNavigation(pageKey);
final generation = ++_navigationGeneration;
bool callbackExecuted = false;
Timer? loadTimer;
void fireEvents() {
if (callbackExecuted) return;
callbackExecuted = true;
loadTimer?.cancel();
if (generation != _navigationGeneration) return;
if (pageKey == _lastReportedNavigationKey) return;
_lastReportedNavigationKey = pageKey;
try {
final pageLoadTime =
DateTime.now().difference(tapTime).inMilliseconds;
track(AppPageViewEvent(
userType: AnalyticsSdk.userTypeProvider(),
pageKey: pageKey,
pageName: pageName,
referrerPageKey: referrerKey,
referrerPageName: referrerName,
currentPageKey: pageKey,
currentPageName: pageName,
pageLoadTime: pageLoadTime,
pageEnterTs: tapTime.millisecondsSinceEpoch ~/ 1000,
));
track(NavigationEvent(
navigationKey: pageKey,
navigationName: pageName,
));
Logger.analyticsSdk(
'trackNavigation() 上报成功: pageKey=$pageKey, referrer=$referrerKey, pageLoadTime=${pageLoadTime}ms',
);
} catch (e) {
Logger.analyticsSdk('trackNavigation() post-frame 上报异常,已安全处理: $e');
}
}
try {
WidgetsBinding.instance.addPostFrameCallback((_) => fireEvents());
loadTimer = Timer(SdkConfig.pageLoadTimeout, fireEvents);
} catch (e) {
Logger.analyticsSdk('trackNavigation() WidgetsBinding 未初始化,降级同步上报: $e');
fireEvents();
}
} catch (e) {
Logger.analyticsSdk('trackNavigation() 异常,已安全处理: $e');
}
}
/// 手动同步当前页面状态,专用于 PageView + AutomaticKeepAliveClientMixin 场景。
///
/// [PageView] 内 Tab 切换不经过路由系统,[PageLifecycleObserver] 无法感知;
/// 在 `PageView.onPageChanged` 中调用此方法,确保后续 page_click 的 `page_key`
/// 归属到实际可见的子页面,而非父路由 key。
///
/// 内部调用 [PageLifecycleObserver.recordNavigation],同时更新
/// [PageLifecycleObserver.currentPageKey] 和栈顶的 effectivePageKey;
/// pop 返回时会自动恢复到最后一次调用的 pageKey。
///
/// - [pageKey]:当前可见子页面的唯一标识,建议使用 [AppPageEnum](如 `AppPageEnum.videoList.key`),
/// 也可传入自定义字符串
/// - [pageName]:当前可见子页面的展示名称(仅用于日志,不上报事件)
///
/// 本方法永远不会抛出异常。
void updateCurrentPage({
required String pageKey,
required String pageName,
}) {
try {
PageLifecycleObserver.recordNavigation(pageKey);
Logger.analyticsSdk(
'updateCurrentPage: pageKey=$pageKey, pageName=$pageName');
} catch (e) {
Logger.analyticsSdk('updateCurrentPage() 异常,已安全处理: $e');
}
}
/// 手动上报任意事件(事件对象必须有 toJson() 方法)。
///
/// 本方法永远不会抛出异常,即使事件无效也不影响应用运行。
void track(dynamic event) {
try {
unawaited(EnvironmentInfoUtil.refreshBatteryLevelIfNeeded());
if (event == null) {
Logger.analyticsSdk('事件为 null,跳过');
return;
}
final adResult = _TrackPipeline.deduplicateAdImpression(event);
if (adResult == null) return;
event = adResult.processedEvent;
final json = _TrackPipeline.serializeEvent(event);
if (json == null) return;
final validated = _TrackPipeline.validateEvent(json);
if (validated == null) return;
if (!_TrackPipeline.checkEventSize(validated)) return;
if (!_TrackPipeline.isEventTypeEnabled(validated)) return;
final accepted = _eventProcessor.enqueueEvent(validated);
if (accepted &&
adResult.adsToMark != null &&
adResult.adsToMark!.isNotEmpty) {
try {
AdImpressionManager.instance.markAsReportedBatch(adResult.adsToMark!,
pageKey: adResult.pageKey);
} catch (e) {
Logger.analyticsSdk('标记广告ID为已上报失败: $e');
}
}
if (accepted &&
adResult.sdkTraceToMark != null &&
adResult.sdkTraceToMark!.isNotEmpty) {
try {
AdImpressionManager.instance
.markSdkTraceReported(adResult.sdkTraceToMark!);
} catch (e) {
Logger.analyticsSdk('标记广告 SDK 曝光 trace_id 失败: $e');
}
}
} catch (e) {
Logger.analyticsSdk('track() 发生异常,已安全处理: $e');
}
}
/// 使用事件名和业务 payload 上报事件。
///
/// 公共字段与 event_id 由埋点 SDK 统一生成,调用方无需构造事件实体。
/// 本方法永远不会抛出异常,即使参数无效也不影响应用运行。
void trackEvent({
required String eventName,
required Map<String, dynamic> payload,
}) {
try {
final normalizedEventName = eventName.trim();
if (normalizedEventName.isEmpty) {
Logger.analyticsSdk('trackEvent() eventName 为空,跳过');
return;
}
final payloadCopy = Map<String, dynamic>.from(payload);
if (normalizedEventName == EventType.adImpressionSdk.event) {
track(AdImpressionSdkEvent(
traceId: payloadCopy['trace_id']?.toString() ?? '',
requestId: payloadCopy['request_id']?.toString() ?? '',
adSlotKey: payloadCopy['ad_slot_key']?.toString() ?? '',
adId: payloadCopy['ad_id']?.toString() ?? '',
adType: payloadCopy['ad_type']?.toString() ?? '',
materialSize: payloadCopy['material_size']?.toString() ?? '',
));
return;
}
final commonFields = SdkContext.generateCommonFields(normalizedEventName);
final sortedPayloadKeys = payloadCopy.keys.toList()..sort();
final eventId = SdkContext.generateEventIdFromCommonFields(
commonFields,
[
jsonEncode({
for (final key in sortedPayloadKeys) key: payloadCopy[key],
}),
],
);
track({
...commonFields,
'event_id': eventId,
'payload': payloadCopy,
});
} catch (e) {
Logger.analyticsSdk('trackEvent() 发生异常,已安全处理: $e');
}
}
Future<void> _trackSdkInit({required DateTime initStartedAt}) async {
if (_sdkInitReported) {
Logger.analyticsSdk('sdk_init 当前进程已上报,跳过重复启动快照');
return;
}
try {
final platformFields = {
...await collectSdkInitPlatformFields(),
'activation_time': await SdkActivationStore.getOrCreateActivationTime(),
};
_applySdkInitEnvironmentOverrides(platformFields);
final commonFields =
SdkContext.generateCommonFields(EventType.sdkInit.event);
final sdkInitSnapshotFields = {
...commonFields,
...SdkContext.getEnvironmentSnapshotFields(),
};
final payload = SdkInitPayloadBuilder.build(
commonFields: sdkInitSnapshotFields,
platformFields: platformFields,
isWeb: kIsWeb,
);
final fingerprint =
DeviceFingerprintUtil.generateV3FromSdkInitPayload(payload);
if (fingerprint.isNotEmpty) {
SdkContext.configureGeneratedDeviceFingerprint(fingerprint);
}
final sdkInitEvent = SdkInitEvent(payload: payload);
track(sdkInitEvent);
_sdkInitReported = true;
_trackInitialAppLifecycle(
initStartedAt: initStartedAt,
platformFields: platformFields,
);
_registerWebLifecycleUnload(initStartedAt);
Logger.analyticsSdk('sdk_init 上报已入队');
} catch (e) {
Logger.analyticsSdk('sdk_init 上报失败,已安全处理: $e');
}
}
void _trackInitialAppLifecycle({
required DateTime initStartedAt,
required Map<String, dynamic> platformFields,
}) {
try {
final durationMs =
DateTime.now().difference(initStartedAt).inMilliseconds;
if (kIsWeb) {
track(AppLifecycleEvent.webLoad(
pageUrl: platformFields['current_url']?.toString() ?? '',
pageLoadTimeMs: durationMs,
viewportWidth: _intField(platformFields['viewport_width'], -1),
viewportHeight: _intField(platformFields['viewport_height'], -1),
));
} else {
track(AppLifecycleEvent.coldStart(
coldStartDurationMs: durationMs,
lastExitType:
platformFields['last_exit_type']?.toString() ?? 'unknown',
bootTime: _intField(platformFields['boot_time'], 0),
));
}
} catch (e) {
Logger.analyticsSdk('app_lifecycle 初始事件上报失败,已安全处理: $e');
track(AppLifecycleEvent.unknown(unknownReason: 'collect_error'));
}
}
void _registerWebLifecycleUnload(DateTime pageEnterTime) {
try {
_webLifecycleUnloadDisposer?.call();
_webLifecycleUnloadDisposer = registerAppLifecycleWebUnloadListener(
track: track,
pageEnterTime: pageEnterTime,
);
} catch (e) {
Logger.analyticsSdk('Web 生命周期卸载监听注册失败,已安全处理: $e');
}
}
int _intField(dynamic value, int fallback) {
if (value is int) return value;
if (value is num) return value.round();
return int.tryParse(value?.toString() ?? '') ?? fallback;
}
void _trackSessionSummary({required String sessionEndReason}) {
final currentSid = SessionManager.instance.currentSessionId ?? '';
if (currentSid.isEmpty || _lastSessionSummarySid == currentSid) return;
final previousSidProvider = SdkContext.sidProvider;
try {
SdkContext.sidProvider = () => currentSid;
final summaryEvent = AppSessionSummaryEvent(
sessionEndReason: sessionEndReason,
endedAt: SessionManager.instance.sessionEndTimeForNotification ??
DateTime.now(),
);
final serialized = _TrackPipeline.serializeEvent(summaryEvent);
if (serialized == null) return;
final validated = _TrackPipeline.validateEvent(serialized);
if (validated == null) return;
final summaryJson = validated;
if (!_TrackPipeline.checkEventSize(summaryJson)) return;
if (!_TrackPipeline.isEventTypeEnabled(summaryJson)) return;
final accepted = _eventProcessor.enqueueEvent(summaryJson);
if (accepted) {
_lastSessionSummarySid = currentSid;
}
} catch (e) {
Logger.analyticsSdk('app_session_summary 上报失败,已安全处理: $e');
} finally {
SdkContext.sidProvider = previousSidProvider;
}
}
/// Debug/QA only: 模拟旧 sid idle 超时,触发 app_session_summary 后创建新 sid。
///
/// Release 包中返回 false 且不做任何事。
Future<bool> debugTriggerIdleSessionSummaryForQa({
Duration idleFor = const Duration(minutes: 31),
}) async {
if (kReleaseMode) return false;
try {
final oldSid = SessionManager.instance.getSessionId();
final armed =
SessionManager.instance.debugSetBackgroundElapsedForQa(idleFor);
if (!armed) return false;
SessionManager.instance.onAppForeground();
await flush();
return SessionManager.instance.currentSessionId != oldSid;
} catch (e) {
Logger.analyticsSdk('QA 触发 app_session_summary 失败: $e');
return false;
}
}
void _applySdkInitEnvironmentOverrides(Map<String, dynamic> platformFields) {
const keys = <String>{
'timezone',
'package_name',
'carrier',
'network_type',
'screen_width',
'screen_height',
'screen_dpi',
'device_pixel_ratio',
'orientation',
'screen_brightness',
'volume',
'is_screen_on',
'battery_level',
'is_charging',
'battery_state',
'cpu_cores',
'device_memory_gb',
'touch_support',
'max_touch_points',
};
final overrides = <String, dynamic>{};
for (final key in keys) {
final value = platformFields[key];
if (_isUsefulSdkInitEnvironmentValue(value)) {
overrides[key] = value;
}
}
SdkContext.updateEnvironmentFields(overrides);
}
bool _isUsefulSdkInitEnvironmentValue(dynamic value) {
if (value == null) return false;
if (value is num) return value >= 0;
final text = value.toString().trim();
return text.isNotEmpty && text != 'unknown';
}
/// 强制立即上报。
///
/// 本方法永远不会抛出异常。
Future<void> flush() async {
try {
await _eventProcessor.flush();
} catch (e) {
Logger.analyticsSdk('flush() 发生异常,已安全处理: $e');
}
}
/// 清理 SDK 资源(先尝试上报剩余事件,再释放资源)。
///
/// 本方法永远不会抛出异常。
Future<void> dispose() async {
try {
_trackSessionSummary(sessionEndReason: 'app_exit');
try {
await flush();
} catch (e) {
Logger.analyticsSdk('dispose() 时上报失败: $e');
}
await _eventProcessor.disposeAsync();
try {
DomainManager.instance.dispose();
} catch (e) {
Logger.analyticsSdk('销毁域名管理器失败: $e');
}
try {
EventTypeConfigManager.instance.dispose();
} catch (e) {
Logger.analyticsSdk('销毁事件类型配置管理器失败: $e');
}
try {
_webLifecycleUnloadDisposer?.call();
_webLifecycleUnloadDisposer = null;
} catch (e) {
Logger.analyticsSdk('销毁 Web 生命周期监听失败: $e');
}
try {
AppLifecycleObserver.instance.dispose();
} catch (e) {
Logger.analyticsSdk('销毁应用生命周期观察者失败: $e');
}
try {
await EnvironmentInfoUtil.dispose();
} catch (e) {
Logger.analyticsSdk('销毁环境字段监听失败: $e');
}
try {
await SessionSensorSampler.instance.dispose();
} catch (e) {
Logger.analyticsSdk('销毁传感器采样器失败: $e');
}
try {
SessionBehaviorMonitor.instance.dispose();
} catch (e) {
Logger.analyticsSdk('销毁行为监听器失败: $e');
}
try {
_pageObserver?.dispose();
_sessionObserver?.dispose();
_pageObserver = null;
_sessionObserver = null;
} catch (e) {
Logger.analyticsSdk('销毁页面生命周期观察者失败: $e');
}
} catch (e) {
Logger.analyticsSdk('dispose() 发生异常: $e');
}
}
// ==================== 调试 ====================
/// 更新用户类型(登录、升级会员、切换账号时调用)。
///
/// 本方法永远不会抛出异常。
void updateUserType(String newType) {
try {
UserManager.instance.updateUserType(newType);
} catch (e) {
Logger.analyticsSdk('更新用户类型失败: $e');
}
}
/// 在 init() 之后、需要更新配置时调用。
///
/// [encryptedConfig] 格式与 init() 中的同名参数一致。
/// 本方法永远不会抛出异常。
Future<void> refreshDomainConfig({
String? encryptedConfig,
}) async {
try {
if (encryptedConfig == null || encryptedConfig.isEmpty) {
Logger.analyticsSdk('refreshDomainConfig encryptedConfig 为空,忽略本次刷新',
level: LogLevel.warn);
return;
}
await _applyEncryptedConfig(
encryptedConfig: encryptedConfig,
source: 'refreshDomainConfig',
);
} catch (e) {
Logger.analyticsSdk('refreshDomainConfig 异常: $e', level: LogLevel.warn);
}
}
/// 测试包用:获取埋点调试信息
Map<String, String> getDebugInfo() {
try {
final steps = getDebugSteps();
final lastStep = steps.isNotEmpty ? steps.last : <String, String>{};
return {
'inited': steps.isNotEmpty && steps.length >= 2
? steps[1]['status'] ?? '未知'
: '未知',
'queueLength': '${_eventProcessor.queueLength}',
'reportUrl': _eventProcessor.debugReportUrl != null
? (_eventProcessor.debugReportUrl!.length > 40
? '${_eventProcessor.debugReportUrl!.substring(0, 40)}...'
: _eventProcessor.debugReportUrl!)
: '未配置',
'debugReportEventLogPath':
_eventProcessor.debugReportEventLogPath ?? '不可用',
'lastStep': lastStep['name'] ?? '',
'lastStatus': lastStep['status'] ?? '',
};
} catch (e) {
Logger.analyticsSdk('getDebugInfo 异常,已安全处理: $e', level: LogLevel.warn);
return {};
}
}
/// 测试包用:获取埋点流程步骤
List<Map<String, String>> getDebugSteps() {
try {
final dm = DomainManager.instance;
final ep = _eventProcessor;
final steps = <Map<String, String>>[];
final hasDomains = dm.reportDomains.isNotEmpty;
steps.add({
'name': '①域名配置',
'status': hasDomains ? '✓已配置' : '✗未配置',
'detail': hasDomains ? '由业务方传入' : '未传入 encryptedConfig',
});
final appIdOk = SdkContext.appId != null && SdkContext.appId!.isNotEmpty;
steps.add({
'name': '②SDK初始化',
'status': appIdOk ? '✓已初始化' : '✗未初始化',
'detail': appIdOk ? 'appId已设置' : '-',
});
final domains = dm.reportDomains;
steps.add({
'name': '③域名列表',
'status': domains.isEmpty
? (appIdOk ? '✗未配置或测速中' : '-')
: '✓已配置(${domains.length}个)',
'detail': domains.isEmpty ? '-' : domains.join(','),
});
final fastest = dm.fastestDomain;
String speedStatus;
if (fastest != null && fastest.isNotEmpty) {
speedStatus = '✓已选';
} else if (domains.isNotEmpty) {
speedStatus = '✗测速失败或进行中';
} else {
speedStatus = '-';
}
steps.add({
'name': '④域名测速',
'status': speedStatus,
'detail': fastest ?? '-',
});
final hasUrl = ep.debugReportUrl != null && ep.debugReportUrl!.isNotEmpty;
String urlDetail = '-';
if (hasUrl && ep.debugReportUrl != null) {
try {
final uri = Uri.parse(ep.debugReportUrl!);
urlDetail = '${uri.host}${uri.path}';
} catch (_) {
urlDetail = '已配置';
}
}
steps.add({
'name': '⑤上报地址',
'status': hasUrl ? '✓已就绪' : '✗未就绪',
'detail': urlDetail,
});
steps.add({
'name': '⑥待上报队列',
'status': '${ep.queueLength}条',
'detail': ep.queueLength > 0 ? '有事件待上报' : '空',
});
String uploadStatus;
String uploadDetail = '-';
if (ep.lastUploadTime == null) {
uploadStatus = '未尝试';
} else if (ep.lastUploadSuccess == true) {
uploadStatus = '✓成功';
uploadDetail = _DebugInspector.formatTime(ep.lastUploadTime);
} else {
uploadStatus = '✗失败';
uploadDetail = ep.lastUploadError ?? '未知';
}
steps.add({
'name': '⑦最近上报',
'status': uploadStatus,
'detail': uploadDetail,
});
return steps;
} catch (e) {
Logger.analyticsSdk('getDebugSteps 异常,已安全处理: $e', level: LogLevel.warn);
return [];
}
}
/// 验证加密配置字符串格式(仅 debug 包可用)
///
/// 支持以下解密后格式:
/// - JSON 数组:`["https://api.example.com", ...]`(域名列表或事件类型列表)
/// - JSON 对象含 `domainList`:`{"domainList":[...]}`
/// - JSON 对象含 `eventList`:`{"eventList":[...]}`
/// - JSON 对象含 `enabled_event_types`:`{"enabled_event_types":[...]}`
Map<String, dynamic> validateEncryptedConfig(String ciphertext) {
return _DebugInspector.validateEncryptedConfig(ciphertext);
}
// ==================== 私有方法 ====================
/// 用户类型提供者
static String Function() userTypeProvider =
() => UserManager.instance.userType;
/// 准备设备指纹状态。
///
/// v3 指纹必须在 sdk_init payload 采集完整后计算,这里只清理旧缓存并
/// 提前写入版本号,避免 re-init 复用旧设备参数。
void _prepareDeviceFingerprint() {
try {
DeviceFingerprintUtil.clearCache();
SdkContext.resetGeneratedDeviceFingerprint();
Logger.analyticsSdk('设备指纹状态已准备,规则版本: ${DeviceFingerprintUtil.kVersion}');
} catch (e) {
Logger.analyticsSdk('设备指纹状态准备失败,device_fingerprint 将为空: $e');
}
}
/// 解密并应用域名配置与事件类型配置(供 init() 和 refreshDomainConfig() 复用)
///
/// [encryptedConfig] 解密后格式:`{"domainList":[...],"eventList":[...]}`
Future<void> _applyEncryptedConfig({
String? encryptedConfig,
required String source,
}) async {
final appId = SdkContext.appId ?? '';
if (encryptedConfig != null && encryptedConfig.isNotEmpty) {
Logger.analyticsSdk(
'$source 收到 encryptedConfig: length=${encryptedConfig.length}');
DomainManager.instance.reset();
// ── 阶段 1:域名配置(独立 try-catch,异常不影响阶段 2)──────────────
List<String>? parsedEventList;
try {
final plaintext = AesGcmUtil.decryptResponseAuto(encryptedConfig);
Logger.analyticsSdkDeveloperLog(
'$source encryptedConfig 解密成功,明文(敏感字段已脱敏): ${_redactEncryptedConfigForLog(plaintext)}',
);
final raw = jsonDecode(plaintext);
// 纯 JSON 数组:直接作为域名列表处理,不含 eventList
if (raw is List) {
final domains = raw
.map<String>((e) => e.toString().trim())
.where((e) => e.isNotEmpty)
.toList();
if (domains.isNotEmpty) {
_startSpeedTest(domains, appId);
Logger.analyticsSdk(
'$source encryptedConfig 解析成功: type=List, domainList=${domains.length}, eventList=0');
} else {
Logger.analyticsSdk('$source encryptedConfig 解析结果为空数组,尝试加载本地缓存',
level: LogLevel.warn);
await _applyDomainCache(appId);
}
// 纯数组格式不含 eventList,parsedEventList 保持 null(走缓存)
} else if (raw is! Map<String, dynamic>) {
Logger.analyticsSdk(
'$source encryptedConfig 解析格式异常:期望 Map,实际 ${raw.runtimeType},尝试加载本地缓存',
level: LogLevel.warn);
await _applyDomainCache(appId);
// parsedEventList 保持 null,走缓存逻辑(阶段 2 处理)
} else {
final decoded = raw;
// 解析 domainList
final rawDomains = decoded['domainList'];
final domains = rawDomains is List
? rawDomains
.map<String>((e) => e.toString().trim())
.where((e) => e.isNotEmpty)
.toList()
: <String>[];
if (domains.isNotEmpty) {
_startSpeedTest(domains, appId);
} else {
Logger.analyticsSdk('domainList 解析结果为空,尝试加载本地缓存',
level: LogLevel.warn);
await _applyDomainCache(appId);
}
// 捕获 eventList 供阶段 2 使用
final rawEvents =
decoded['eventList'] ?? decoded['enabled_event_types'];
if (rawEvents is List && rawEvents.isNotEmpty) {
parsedEventList = rawEvents.map((e) => e.toString()).toList();
}
_applyEventSignatureConfig(decoded);
Logger.analyticsSdk(
'$source encryptedConfig 解析成功: type=Map, domainList=${domains.length}, eventList=${parsedEventList?.length ?? 0}, eventSignature=${_hasEventSignatureConfig(decoded)}');
}
} catch (e) {
Logger.analyticsSdk('$source encryptedConfig 解密/解析失败: $e,尝试加载本地缓存',
level: LogLevel.warn);
await _applyDomainCache(appId);
}
// ── 阶段 2:事件类型配置(独立 try-catch,异常不触发第二轮测速)──────
try {
if (parsedEventList != null) {
await EventTypeConfigManager.instance
.initWithConfig(jsonEncode(parsedEventList));
} else {
Logger.analyticsSdk('eventList 解析结果为空,尝试加载本地缓存',
level: LogLevel.warn);
await EventTypeConfigManager.instance.loadCachedConfig();
}
} catch (e) {
Logger.analyticsSdk('eventList 初始化失败: $e,尝试加载本地缓存',
level: LogLevel.warn);
await EventTypeConfigManager.instance.loadCachedConfig();
}
} else {
Logger.analyticsSdk('$source encryptedConfig 未传入,尝试加载本地缓存',
level: LogLevel.warn);
await _applyDomainCache(appId);
await EventTypeConfigManager.instance.loadCachedConfig();
}
}
String _redactEncryptedConfigForLog(String plaintext) {
try {
final decoded = jsonDecode(plaintext);
return jsonEncode(_redactSensitiveConfigValue(decoded));
} catch (_) {
return plaintext;
}
}
dynamic _redactSensitiveConfigValue(dynamic value) {
const sensitiveKeys = {
'signKey',
'eventSignKey',
'event_signature_key',
'event_sig_key',
'hmacKey',
'key',
};
if (value is Map) {
return value.map((key, child) {
final keyText = key.toString();
if (sensitiveKeys.contains(keyText)) {
return MapEntry(keyText, '***');
}
return MapEntry(keyText, _redactSensitiveConfigValue(child));
});
}
if (value is List) {
return value.map(_redactSensitiveConfigValue).toList();
}
return value;
}
bool _hasEventSignatureConfig(Map<String, dynamic> decoded) {
if (_firstConfigString(decoded, const [
'signKey',
'eventSignKey',
'event_signature_key',
'event_sig_key',
]) !=
null) {
return true;
}
final rawEventSignature = decoded['eventSignature'];
if (rawEventSignature is Map) {
final eventSignature = Map<String, dynamic>.from(rawEventSignature);
return _firstConfigString(eventSignature, const [
'signKey',
'key',
'hmacKey',
]) !=
null;
}
return false;
}
void _applyEventSignatureConfig(Map<String, dynamic> decoded) {
try {
final directSignKey = _firstConfigString(decoded, const [
'signKey',
'eventSignKey',
'event_signature_key',
'event_sig_key',
]);
final directVersion = _firstConfigString(decoded, const [
'signVersion',
'eventSigVersion',
'event_sig_version',
]);
String? nestedSignKey;
String? nestedVersion;
final rawEventSignature = decoded['eventSignature'];
if (rawEventSignature is Map) {
final eventSignature = Map<String, dynamic>.from(rawEventSignature);
nestedSignKey = _firstConfigString(eventSignature, const [
'signKey',
'key',
'hmacKey',
]);
nestedVersion = _firstConfigString(eventSignature, const [
'version',
'signVersion',
'eventSigVersion',
'event_sig_version',
]);
}
final signKey = directSignKey ?? nestedSignKey;
if (signKey == null) return;
SdkContext.configureEventSignature(
signKey: signKey,
version: directVersion ?? nestedVersion,
);
} catch (e) {
Logger.analyticsSdk('签名配置解析失败,事件签名将保持当前状态: $e', level: LogLevel.warn);
}
}
String? _firstConfigString(
Map<String, dynamic> decoded,
List<String> keys,
) {
for (final key in keys) {
final value = decoded[key];
if (value is String && value.trim().isNotEmpty) {
return value.trim();
}
}
return null;
}
Future<void> _applyDomainCache(String appId) async {
try {
final cachedDomains = await DomainManager.instance.loadCachedDomains();
if (cachedDomains.isNotEmpty) {
Logger.analyticsSdk('使用缓存域名降级: $cachedDomains');
_startSpeedTest(cachedDomains, appId);
} else {
Logger.analyticsSdk(
'无可用域名缓存,上报 URL 保持未配置,事件将在 refreshDomainConfig() 后上报',
level: LogLevel.warn);
}
} catch (e) {
Logger.analyticsSdk('加载域名缓存失败: $e', level: LogLevel.warn);
}
}
/// 设置域名测速回调并启动测速(fire-and-forget)
void _startSpeedTest(List<String> domains, String appId) {
DomainManager.instance.onFastestDomainChanged = (fastest) {
try {
if (fastest != null && fastest.isNotEmpty) {
final url = ApiConfig.getReportUrl(fastest, appId: appId);
_eventProcessor.updateReportUrl(url);
Logger.analyticsSdk('域名测速完成,已更新上报URL: $url');
}
} catch (e) {
Logger.analyticsSdk('_startSpeedTest 回调异常,已安全处理: $e',
level: LogLevel.warn);
}
};
DomainManager.instance.initWithDomains(domains);
}
}
/// 广告展示去重结果(仅在 [AnalyticsSdk] 内部使用)
class _AdDedupeResult {
/// 经过去重处理后的事件(可能是过滤了部分 adId 的 [AdImpressionEvent],或原始事件)
final dynamic processedEvent;
/// 成功入队后需要标记为已上报的 adId 字符串;非广告展示事件时为 null
final String? adsToMark;
/// 广告所在页面的 pageKey,用于页面级去重;非广告事件时为空字符串
final String pageKey;
/// 成功入队后需要标记为已上报的广告 SDK trace_id。
final String? sdkTraceToMark;
const _AdDedupeResult(
this.processedEvent,
this.adsToMark, [
this.pageKey = '',
this.sdkTraceToMark,
]);
}
/// track() 管道辅助方法(纯静态,不依赖 AnalyticsSdk 实例)
class _TrackPipeline {
/// 广告展示事件去重处理。返回 null 表示全部 adId 均已上报,应跳过整条事件。
static _AdDedupeResult? deduplicateAdImpression(dynamic event) {
if (event is AdImpressionSdkEvent) {
return _deduplicateAdSdkImpression(event);
}
if (event is! AdImpressionEvent) {
return _AdDedupeResult(event, null);
}
final AdImpressionEvent adEvent = event;
try {
final unreportedAdIds = _getUnreportedAdIds(adEvent);
if (unreportedAdIds != null && unreportedAdIds.isEmpty) {
Logger.analyticsSdk('广告展示事件已上报,跳过: ${adEvent.adId}');
return null;
}
if (unreportedAdIds != null) {
final originalAdIdsCount = adEvent.adId
.split(',')
.map((id) => id.trim())
.where((id) => id.isNotEmpty)
.length;
if (unreportedAdIds.length < originalAdIdsCount) {
final filtered = _createFilteredAdEvent(adEvent, unreportedAdIds);
Logger.analyticsSdk(
'广告展示事件已过滤: 原始$originalAdIdsCount个ID,只上报${unreportedAdIds.length}个未上报的ID: ${filtered.adId}');
return _AdDedupeResult(filtered, filtered.adId, filtered.pageKey);
}
}
return _AdDedupeResult(adEvent, adEvent.adId, adEvent.pageKey);
} catch (e) {
Logger.analyticsSdk('广告去重检查异常,跳过去重: $e');
return _AdDedupeResult(adEvent, adEvent.adId, adEvent.pageKey);
}
}
/// 广告 SDK 曝光以服务端下发的 trace_id 为单次投放标识,在同一 session 内去重。
static _AdDedupeResult? _deduplicateAdSdkImpression(
AdImpressionSdkEvent event,
) {
final traceId = event.traceId.trim();
if (traceId.isEmpty) return _AdDedupeResult(event, null);
if (AdImpressionManager.instance.isSdkTraceReported(traceId)) {
Logger.analyticsSdk('广告 SDK 曝光已上报,跳过: $traceId');
return null;
}
return _AdDedupeResult(event, null, '', traceId);
}
/// 将事件对象序列化为 JSON Map。返回 null 表示序列化失败,应丢弃事件。
static Map<String, dynamic>? serializeEvent(dynamic event) {
try {
final json = event is Map<String, dynamic> ? event : event.toJson();
if (json == null) {
Logger.analyticsSdk('事件 toJson() 返回 null,跳过');
return null;
}
return SdkContext.attachDeviceEnvIfNeeded(json);
} catch (e) {
Logger.analyticsSdk('事件序列化失败,跳过: $e');
return null;
}
}
/// 校验事件字段。返回 null 表示校验不通过,应丢弃事件。
static Map<String, dynamic>? validateEvent(Map<String, dynamic> json) {
final validated = EventValidator.validate(json);
if (validated == null) {
Logger.analyticsSdk('事件校验不通过,已丢弃: ${json['event']}',
level: LogLevel.warn);
}
return validated;
}
/// 检查单条事件大小。返回 false 表示超限应丢弃。
static bool checkEventSize(Map<String, dynamic> json) {
try {
final encoded = jsonEncode(json);
final sizeBytes = utf8.encode(encoded).length;
if (sizeBytes > SdkConfig.maxSingleEventSize) {
final sizeKB = (sizeBytes / 1024).toStringAsFixed(1);
final limitKB =
(SdkConfig.maxSingleEventSize / 1024).toStringAsFixed(0);
Logger.analyticsSdk(
'事件过大 (${sizeKB}KB > ${limitKB}KB 限制),丢弃。event=${json['event']}',
level: LogLevel.warn);
return false;
}
return true;
} catch (e) {
Logger.analyticsSdk('事件大小检查失败,继续上报: $e');
return true;
}
}
/// 检查事件类型是否在配置中启用。返回 false 表示未启用应跳过。
static bool isEventTypeEnabled(Map<String, dynamic> json) {
try {
final eventType = json['event'] as String?;
if (eventType != null &&
!EventTypeConfigManager.instance.isEventTypeEnabled(eventType)) {
Logger.analyticsSdk('事件类型 $eventType 未在配置中启用,跳过上报');
return false;
}
return true;
} catch (e) {
Logger.analyticsSdk('事件类型检查失败,继续上报: $e');
return true;
}
}
static List<String>? _getUnreportedAdIds(AdImpressionEvent event) {
try {
if (event.adId.isEmpty) return null;
final trimmedAdId = event.adId.trim();
if (trimmedAdId.isEmpty) return null;
return AdImpressionManager.instance
.getUnreportedAdIds(trimmedAdId, pageKey: event.pageKey);
} catch (e) {
Logger.analyticsSdk('广告去重检查异常,跳过: $e');
return null;
}
}
static AdImpressionEvent _createFilteredAdEvent(
AdImpressionEvent originalEvent, List<String> unreportedIds) {
try {
final originalAdIds = originalEvent.adId
.split(',')
.map((id) => id.trim())
.where((id) => id.isNotEmpty)
.toList();
List<String>? originalCreativeIds;
if (originalEvent.creativeId.isNotEmpty) {
originalCreativeIds =
originalEvent.creativeId.split(',').map((id) => id.trim()).toList();
}
final filteredAdIds = <String>[];
final filteredCreativeIds = <String>[];
for (int i = 0; i < originalAdIds.length; i++) {
if (unreportedIds.contains(originalAdIds[i])) {
filteredAdIds.add(originalAdIds[i]);
if (originalCreativeIds != null && i < originalCreativeIds.length) {
filteredCreativeIds.add(originalCreativeIds[i]);
} else {
filteredCreativeIds.add('');
}
}
}
return AdImpressionEvent(
pageKey: originalEvent.pageKey,
pageName: originalEvent.pageName,
adSlotKey: originalEvent.adSlotKey,
requestId: originalEvent.requestId,
adId: filteredAdIds.join(','),
materialSize: originalEvent.materialSize,
creativeId: originalEvent.creativeId.isNotEmpty
? filteredCreativeIds.join(',')
: "",
adType: originalEvent.adType,
);
} catch (e) {
Logger.analyticsSdk('创建过滤后的广告事件失败,使用原始事件: $e');
return originalEvent;
}
}
}
/// Debug 调试辅助方法(纯静态,不依赖 AnalyticsSdk 实例)
class _DebugInspector {
static String formatTime(DateTime? dt) {
if (dt == null) return '-';
return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}';
}
/// 验证加密配置字符串格式(仅 debug 包可用)
static Map<String, dynamic> validateEncryptedConfig(String ciphertext) {
if (kReleaseMode) {
return {'success': false, 'preview': null, 'error': 'release 包不可用'};
}
try {
final plaintext = AesGcmUtil.decryptResponseAuto(ciphertext);
final raw = jsonDecode(plaintext);
if (raw is List) {
if (raw.isNotEmpty) {
return {
'success': true,
'preview': raw[0].toString(),
'count': raw.length,
'error': null,
};
}
final preview =
plaintext.length > 60 ? plaintext.substring(0, 60) : plaintext;
return {
'success': false,
'preview': preview,
'error': '解密成功但列表为空(0 个元素),请检查配置',
};
}
if (raw is! Map<String, dynamic>) {
return {
'success': false,
'preview':
plaintext.length > 60 ? plaintext.substring(0, 60) : plaintext,
'error': '解密成功但格式错误:应为 JSON 数组或对象,实际为 ${raw.runtimeType}',
};
}
final decoded = raw;
List? items;
if (decoded['domainList'] is List) {
items = decoded['domainList'] as List;
} else if (decoded['eventList'] is List) {
items = decoded['eventList'] as List;
} else if (decoded['enabled_event_types'] is List) {
items = decoded['enabled_event_types'] as List;
}
if (items != null && items.isNotEmpty) {
return {
'success': true,
'preview': items[0].toString(),
'count': items.length,
'error': null,
};
}
final preview =
plaintext.length > 60 ? plaintext.substring(0, 60) : plaintext;
return {
'success': false,
'preview': preview,
'error': '解密成功但格式无法识别(0 个元素),请检查 JSON 格式',
};
} catch (e) {
return {'success': false, 'preview': null, 'error': '解密失败: $e'};
}
}
}