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 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 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 getParams([List? keys]) { try { final all = { '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 payload, }) { try { final normalizedEventName = eventName.trim(); if (normalizedEventName.isEmpty) { Logger.analyticsSdk('trackEvent() eventName 为空,跳过'); return; } final payloadCopy = Map.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 _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 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 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 platformFields) { const keys = { '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 = {}; 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 flush() async { try { await _eventProcessor.flush(); } catch (e) { Logger.analyticsSdk('flush() 发生异常,已安全处理: $e'); } } /// 清理 SDK 资源(先尝试上报剩余事件,再释放资源)。 /// /// 本方法永远不会抛出异常。 Future 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 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 getDebugInfo() { try { final steps = getDebugSteps(); final lastStep = steps.isNotEmpty ? steps.last : {}; 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> getDebugSteps() { try { final dm = DomainManager.instance; final ep = _eventProcessor; final steps = >[]; 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 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 _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? 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((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) { 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((e) => e.toString().trim()) .where((e) => e.isNotEmpty) .toList() : []; 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 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.from(rawEventSignature); return _firstConfigString(eventSignature, const [ 'signKey', 'key', 'hmacKey', ]) != null; } return false; } void _applyEventSignatureConfig(Map 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.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 decoded, List keys, ) { for (final key in keys) { final value = decoded[key]; if (value is String && value.trim().isNotEmpty) { return value.trim(); } } return null; } Future _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 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? serializeEvent(dynamic event) { try { final json = event is Map ? 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? validateEvent(Map json) { final validated = EventValidator.validate(json); if (validated == null) { Logger.analyticsSdk('事件校验不通过,已丢弃: ${json['event']}', level: LogLevel.warn); } return validated; } /// 检查单条事件大小。返回 false 表示超限应丢弃。 static bool checkEventSize(Map 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 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? _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 unreportedIds) { try { final originalAdIds = originalEvent.adId .split(',') .map((id) => id.trim()) .where((id) => id.isNotEmpty) .toList(); List? originalCreativeIds; if (originalEvent.creativeId.isNotEmpty) { originalCreativeIds = originalEvent.creativeId.split(',').map((id) => id.trim()).toList(); } final filteredAdIds = []; final filteredCreativeIds = []; 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 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) { 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'}; } } }