import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:yx_tracking_flutter/src/config/analytics_config.dart'; import 'package:yx_tracking_flutter/src/config/config_manager.dart'; import 'package:yx_tracking_flutter/src/core/analytics_core.dart'; import 'package:yx_tracking_flutter/src/core/interceptors.dart'; import 'package:yx_tracking_flutter/src/core/scheduler.dart'; import 'package:yx_tracking_flutter/src/model/device_info.dart'; import 'package:yx_tracking_flutter/src/model/event.dart'; import 'package:yx_tracking_flutter/src/model/system_dim_info.dart'; import 'package:yx_tracking_flutter/src/network/api_client.dart'; import 'package:yx_tracking_flutter/src/storage/config_storage.dart'; import 'package:yx_tracking_flutter/src/storage/event_storage.dart'; class MemoryEventStorage implements EventStorage { int _nextId = 1; final Map _store = {}; final List insertedEvents = []; @override Future init() async {} @override Future insert(Event event) async { final id = _nextId++; insertedEvents.add(event); _store[id] = StoredEvent( id: id, event: event, retryCount: event.retryCount, createTime: event.createTime, ); return id; } @override Future> fetchBatch(int limit) async { final items = _store.values.toList() ..sort((a, b) => a.createTime.compareTo(b.createTime)); return items.take(limit).toList(growable: false); } @override Future> fetchRecent(int limit) async { final items = _store.values.toList() ..sort((a, b) => b.createTime.compareTo(a.createTime)); return items.take(limit).toList(growable: false); } @override Future deleteByIds(List ids) async { for (final id in ids) { _store.remove(id); } } @override Future count() async => _store.length; @override Future trimToMaxSize(int maxSize) async { if (maxSize <= 0 || _store.length <= maxSize) { return 0; } final items = _store.values.toList() ..sort((a, b) => a.createTime.compareTo(b.createTime)); final overflow = items.length - maxSize; for (var i = 0; i < overflow; i++) { _store.remove(items[i].id); } return overflow; } @override Future deleteExpired(DateTime cutoff) async { final before = _store.length; _store.removeWhere((_, value) => !value.createTime.isAfter(cutoff)); return before - _store.length; } @override Future updateRetryCount(int id, int retryCount) async { final current = _store[id]; if (current == null) { return; } final updatedEvent = current.event.copyWith(retryCount: retryCount); _store[id] = current.copyWith( retryCount: retryCount, event: updatedEvent, ); } @override Future dispose() async {} StoredEvent? byId(int id) => _store[id]; StoredEvent? get singleOrNull { if (_store.length != 1) { return null; } return _store.values.first; } } class BlockingEventStorage extends MemoryEventStorage { BlockingEventStorage(this._blocker); final Completer _blocker; var _blockedOnce = false; @override Future> fetchBatch(int limit) async { if (!_blockedOnce) { _blockedOnce = true; await _blocker.future; } return super.fetchBatch(limit); } } class FakeApiClient extends ApiClient { FakeApiClient(super.config); ApiException? exceptionToThrow; Error? errorToThrow; final List> sentBatches = >[]; @override Future sendBatch(List events) async { sentBatches.add(events); final error = errorToThrow; if (error != null) { throw error; } final exception = exceptionToThrow; if (exception != null) { throw exception; } } } class TestConfigManager extends ConfigManager { TestConfigManager({ required super.config, SystemDimInfo? initialConfig, }) : currentConfigForTesting = initialConfig, super(refreshInterval: Duration.zero); @override SystemDimInfo? get currentConfig => currentConfigForTesting; SystemDimInfo? currentConfigForTesting; @override Future init() async {} @override Future fetchAndCacheConfig({bool force = false}) async {} @override Future forceRefresh() async {} @override Future dispose() async {} } class InMemoryConfigStorage implements ConfigStorage { SystemDimInfo? _info; @override Future init() async {} @override Future saveSystemDimInfo(SystemDimInfo info) async { _info = info; } @override Future loadSystemDimInfo() async => _info; @override Future clear() async { _info = null; } @override Future dispose() async {} } class IntervalConfigManager extends ConfigManager { IntervalConfigManager({ required super.config, required Duration interval, }) : fetchCalls = 0, forceCalls = 0, initCalls = 0, super( refreshInterval: interval, storage: InMemoryConfigStorage(), ); int fetchCalls; int forceCalls; int initCalls; @override Future init() async { initCalls += 1; } @override Future fetchAndCacheConfig({bool force = false}) async { fetchCalls += 1; if (force) { forceCalls += 1; } } @override Future forceRefresh() async { forceCalls += 1; } @override Future dispose() async {} } class NoopScheduler extends Scheduler { NoopScheduler({ required super.interval, required super.onTick, }); @override void start() {} @override void stop() {} } class RecordingInterceptor extends AnalyticsInterceptor { RecordingInterceptor({ this.onBefore, this.onAfter, }); final FutureOr Function(Event event)? onBefore; final FutureOr Function(Event event, SendResult result)? onAfter; int beforeCalls = 0; int afterCalls = 0; final List afterResults = []; @override FutureOr beforeSend(Event event) async { beforeCalls += 1; final handler = onBefore; if (handler == null) { return event; } return handler(event); } @override FutureOr afterSend(Event event, SendResult result) async { afterCalls += 1; afterResults.add(result); final handler = onAfter; if (handler == null) { return; } await handler(event, result); } } const DeviceInfo _testDeviceInfo = DeviceInfo( os: 'test-os', model: 'test-model', screenResolution: '100x200', ); AnalyticsConfig _testConfig({ bool enableDebug = true, int batchSize = 20, int maxRetryCount = 2, int maxCacheSize = 5000, }) { return AnalyticsConfig( systemCode: 'TEST_APP', endpointBaseUrl: 'https://example.com', enableDebug: enableDebug, batchSize: batchSize, flushInterval: 3600, maxRetryCount: maxRetryCount, maxCacheSize: maxCacheSize, enableMetrics: false, ); } SystemDimInfo _dimInfo({ List events = const [], List tags = const [], SdkStrategy? strategy, }) { return SystemDimInfo( systemInfo: const SystemInfo(raw: {}), eventDefinitions: events, tagDefinitions: tags, sdkStrategy: strategy, lastFetchedAt: DateTime.now(), ); } Future _collectTestDeviceInfo() async => _testDeviceInfo; Scheduler _noopScheduler( Duration interval, Future Function() onTick, ) => NoopScheduler(interval: interval, onTick: onTick); Future _waitUntil( bool Function() predicate, { Duration step = const Duration(milliseconds: 5), int maxTries = 40, }) async { for (var i = 0; i < maxTries; i++) { if (predicate()) { return; } await Future.delayed(step); } } String _eventTypeOfStored(StoredEvent stored) => stored.event.eventType; const _allEventDefinitions = [ EventDefinition(eventCode: 'EVENT_A'), EventDefinition(eventCode: 'EVENT_RETRY'), EventDefinition(eventCode: 'EVENT_DROP'), EventDefinition(eventCode: 'EVENT_NO_CONFIG'), EventDefinition(eventCode: 'EVENT_DISABLED'), EventDefinition(eventCode: 'EVENT_OFF'), EventDefinition(eventCode: 'EVENT_SAMPLE'), EventDefinition(eventCode: 'EVENT_INTERCEPT'), EventDefinition(eventCode: 'EVENT_DROP_BY_INTERCEPTOR'), EventDefinition(eventCode: 'EVENT_THROW'), EventDefinition(eventCode: 'EVENT_METRIC'), EventDefinition(eventCode: 'EVENT_OFFLINE'), EventDefinition(eventCode: 'EVENT_STRESS'), ]; Future _drainMicrotasks() async { await Future.delayed(Duration.zero); await Future.delayed(Duration.zero); } void main() { group('AnalyticsCore.flush', () { test('成功发送后删除事件', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final config = _testConfig(batchSize: 5); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_A'); await core.track('EVENT_A'); expect(await storage.count(), 2); await core.flush(force: true); expect(apiClient.sentBatches, isNotEmpty); expect(await storage.count(), 0); await core.dispose(); }); test('可重试失败会增加 retryCount 并保留事件', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final config = _testConfig(batchSize: 10); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); apiClient.exceptionToThrow = const ApiException( message: 'network', retryable: true, ); await core.track('EVENT_RETRY'); await core.flush(force: true); final stored = storage.singleOrNull; expect(stored, isNotNull); expect(stored!.retryCount, 1); apiClient.exceptionToThrow = null; await core.flush(force: true); expect(await storage.count(), 0); await core.dispose(); }); test('不可重试失败会直接删除事件', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final config = _testConfig(batchSize: 10); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); apiClient.exceptionToThrow = const ApiException( message: 'bad request', retryable: false, statusCode: 400, ); await core.track('EVENT_DROP'); await core.flush(force: true); expect(await storage.count(), 0); await core.dispose(); }); }); group('AnalyticsCore.validation (release)', () { test('release 模式会写入校验标记', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final config = _testConfig(enableDebug: false, batchSize: 10); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo( events: _allEventDefinitions, tags: const [ TagDefinition( tagName: 'requiredTag', tagType: 'string', isRequired: true, ), ], ); // 触发两个校验问题:未知事件 + 缺少必填 tag。 await core.track('UNKNOWN_EVENT'); await _drainMicrotasks(); final inserted = storage.insertedEvents.last; final tags = inserted.customTags ?? const {}; expect(tags['_sdk_invalid_event'], true); expect(tags['_sdk_missing_tags'], contains('requiredTag')); await core.dispose(); }); }); group('AnalyticsCore.fallback', () { test('无配置时仍可 track/flush(降级为 Phase 1 行为)', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(batchSize: 10)); await core.track('EVENT_NO_CONFIG'); await core.flush(force: true); expect(apiClient.sentBatches, isNotEmpty); await core.dispose(); }); }); group('AnalyticsCore.strategy', () { test('全局策略关闭时直接丢弃事件', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final config = _testConfig(batchSize: 10); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo( events: _allEventDefinitions, strategy: const SdkStrategy( enabled: false, defaultSampleRate: 1, eventSettings: {}, ), ); await core.track('EVENT_DISABLED'); expect(await storage.count(), 0); await core.dispose(); }); test('事件级策略 disabled 时丢弃事件', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final config = _testConfig(batchSize: 10); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo( events: _allEventDefinitions, strategy: const SdkStrategy( enabled: true, defaultSampleRate: 1, eventSettings: { 'EVENT_OFF': EventStrategy(enabled: false, sampleRate: 1), }, ), ); await core.track('EVENT_OFF'); expect(await storage.count(), 0); await core.dispose(); }); test('采样策略根据随机数决定是否入库', () async { const strategy = SdkStrategy( enabled: true, defaultSampleRate: 1, eventSettings: { 'EVENT_SAMPLE': EventStrategy(enabled: true, sampleRate: 0.5), }, ); final storageDropped = MemoryEventStorage(); late TestConfigManager configManagerDropped; final coreDropped = AnalyticsCore( storageFactory: () => storageDropped, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManagerDropped = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, randomDouble: () => 0.9, ); await coreDropped.init(_testConfig(batchSize: 10)); configManagerDropped.currentConfigForTesting = _dimInfo( events: _allEventDefinitions, strategy: strategy, ); await coreDropped.track('EVENT_SAMPLE'); expect(await storageDropped.count(), 0); await coreDropped.dispose(); final storageKept = MemoryEventStorage(); late TestConfigManager configManagerKept; final coreKept = AnalyticsCore( storageFactory: () => storageKept, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManagerKept = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, randomDouble: () => 0.1, ); await coreKept.init(_testConfig(batchSize: 10)); configManagerKept.currentConfigForTesting = _dimInfo( events: _allEventDefinitions, strategy: strategy, ); await coreKept.track('EVENT_SAMPLE'); expect(await storageKept.count(), 1); await coreKept.dispose(); }); }); group('AnalyticsCore.interceptors', () { test('拦截器可修改事件并收到 afterSend 回调', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final interceptor = RecordingInterceptor( onBefore: (event) { final tags = Map.from( event.customTags ?? const {}, ); tags['intercepted'] = true; return event.copyWith(customTags: tags); }, ); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); core.addInterceptor(interceptor); await core.track('EVENT_INTERCEPT'); await core.flush(force: true); final sentEvent = apiClient.sentBatches.single.single; expect(sentEvent.customTags?['intercepted'], true); expect(interceptor.afterCalls, 1); expect(interceptor.afterResults.single.success, true); await core.dispose(); }); test('拦截器返回 null 会拦截并删除事件', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final interceptor = RecordingInterceptor(onBefore: (_) => null); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); core.addInterceptor(interceptor); await core.track('EVENT_DROP_BY_INTERCEPTOR'); await core.flush(force: true); expect(await storage.count(), 0); await core.dispose(); }); test('拦截器异常不会影响后续拦截器与发送', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final throwing = RecordingInterceptor( onBefore: (_) => throw StateError('boom'), ); final tagging = RecordingInterceptor( onBefore: (event) { final tags = Map.from( event.customTags ?? const {}, ); tags['tagged'] = 'yes'; return event.copyWith(customTags: tags); }, ); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); core ..addInterceptor(throwing) ..addInterceptor(tagging); await core.track('EVENT_THROW'); await core.flush(force: true); final sentEvent = apiClient.sentBatches.single.single; expect(sentEvent.customTags?['tagged'], 'yes'); await core.dispose(); }); }); group('AnalyticsCore.metrics', () { test('reportMetricsNow 会生成指标事件并包含计数', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final config = AnalyticsConfig( systemCode: 'TEST_APP', endpointBaseUrl: 'https://example.com', enableDebug: true, batchSize: 10, flushInterval: 3600, metricsReportInterval: Duration(days: 1), ); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_METRIC'); await core.track('EVENT_METRIC'); await core.flush(force: true); await core.reportMetricsNow(); final metricsSendEvents = storage.insertedEvents .where((e) => e.eventType == 'SDK_METRICS_SEND') .toList(growable: false); expect(metricsSendEvents, isNotEmpty); final metricsParams = metricsSendEvents.last.eventParams ?? const {}; expect(metricsParams['sentCount'], 2); expect(apiClient.sentBatches, isNotEmpty); await core.dispose(); }); test('构造默认依赖且未 init 时 isInitialized 为 false', () { final core = AnalyticsCore(); expect(core.isInitialized, isFalse); }); test('构造时会使用默认的 configManager/scheduler 工厂', () { final core = AnalyticsCore( storageFactory: MemoryEventStorage.new, apiClientFactory: FakeApiClient.new, ); expect(core.isInitialized, isFalse); }); test('重复 init 会先 dispose 再重新初始化', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(enableDebug: false)); await core.init(_testConfig(enableDebug: false)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_REINIT'); expect(await storage.count(), 1); await core.dispose(); }); test('未初始化时 track/flush/cached 查询会被安全忽略', () async { final core = AnalyticsCore( storageFactory: MemoryEventStorage.new, apiClientFactory: FakeApiClient.new, deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.track('EVENT_BEFORE_INIT'); await core.flush(); expect(await core.cachedEventCount(), 0); expect(await core.cachedRecentEvents(limit: 0), isEmpty); await core.refreshConfig(); await core.dispose(); }); test('eventType 为空时会被忽略', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig()); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track(' '); expect(await storage.count(), 0); await core.dispose(); }); test('校验错误且配置阻断时会直接丢弃事件(debug)', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); final config = AnalyticsConfig( systemCode: 'TEST_APP', endpointBaseUrl: 'https://example.com', enableDebug: true, flushInterval: 3600, enableMetrics: false, blockOnValidationError: true, ); await core.init(config); configManager.currentConfigForTesting = _dimInfo(); await core.track('EVENT_BLOCKED'); expect(await storage.count(), 0); await core.dispose(); }); test('超过 maxCacheSize 时会 trim 并计入 dropped', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init( _testConfig( enableDebug: false, maxCacheSize: 1, batchSize: 99, ), ); configManager.currentConfigForTesting = _dimInfo( events: const [ EventDefinition(eventCode: 'EVENT_TRIM'), ], ); await core.track('EVENT_TRIM'); await core.track('EVENT_TRIM'); expect(await storage.count(), 1); await core.dispose(); }); test('达到 batchSize 会触发一次异步 flush', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; late FakeApiClient apiClient; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init( _testConfig( enableDebug: false, batchSize: 1, ), ); configManager.currentConfigForTesting = _dimInfo( events: const [ EventDefinition(eventCode: 'EVENT_AUTO_FLUSH'), ], ); await core.track('EVENT_AUTO_FLUSH'); await _waitUntil(() => apiClient.sentBatches.isNotEmpty); await core.flush(force: true); await core.dispose(); }); test('refreshConfig(force) 会分别调用 forceRefresh 与 fetch', () async { final storage = MemoryEventStorage(); late IntervalConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = IntervalConfigManager( config: c, interval: const Duration(milliseconds: 10), ), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(batchSize: 50)); await core.refreshConfig(); await core.refreshConfig(force: false); expect(configManager.forceCalls, greaterThanOrEqualTo(1)); expect(configManager.fetchCalls, greaterThanOrEqualTo(1)); await core.dispose(); }); test('flush 发生未知异常时会按可重试处理', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; late FakeApiClient apiClient; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(enableDebug: false, batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_THROW_UNKNOWN'); apiClient.errorToThrow = StateError('boom'); await core.flush(force: true); expect(await storage.count(), 1); await core.dispose(); }); test('已有 flush 进行中时会直接跳过', () async { final blocker = Completer(); final storage = BlockingEventStorage(blocker); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(enableDebug: false, batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_CONCURRENT_FLUSH'); final firstFlush = core.flush(force: true); await Future.delayed(const Duration(milliseconds: 10)); await core.flush(); blocker.complete(); await firstFlush; await core.dispose(); }); test('重试次数超过上限时会删除事件', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; late FakeApiClient apiClient; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); const maxRetry = 1; await core.init( _testConfig( enableDebug: false, batchSize: 10, maxRetryCount: maxRetry, ), ); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); final now = DateTime.now(); final event = Event( systemCode: 'TEST_APP', eventType: 'EVENT_MAX_RETRY', userInfo: null, clientType: 3, clientTimestamp: now.millisecondsSinceEpoch, timestamp: now.toIso8601String(), deviceInfo: _testDeviceInfo, eventParams: null, customTags: null, createTime: now, retryCount: maxRetry, ); await storage.insert(event); apiClient.exceptionToThrow = const ApiException( message: 'offline', retryable: true, ); await core.flush(force: true); expect(await storage.count(), 0); await core.dispose(); }); test('afterSend 拦截器抛错不会影响主流程', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; late FakeApiClient apiClient; final interceptor = RecordingInterceptor( onAfter: (_, __) => throw StateError('after boom'), ); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, interceptors: [interceptor], ); await core.init(_testConfig(enableDebug: false, batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_AFTER_THROW'); await core.flush(force: true); expect(apiClient.sentBatches, isNotEmpty); await core.dispose(); }); test('退避时间未到时 flush 会被跳过', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; late FakeApiClient apiClient; var now = DateTime.fromMillisecondsSinceEpoch(1000); final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, now: () => now, ); await core.init(_testConfig(enableDebug: false, batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); await core.track('EVENT_BACKOFF'); apiClient.exceptionToThrow = const ApiException( message: 'offline', retryable: true, ); await core.flush(force: true); final sentBefore = apiClient.sentBatches.length; await core.flush(); expect(apiClient.sentBatches.length, sentBefore); now = now.add(const Duration(minutes: 5)); await core.flush(force: true); await core.dispose(); }); test('release 模式会为类型不匹配追加校验标记', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(enableDebug: false, batchSize: 50)); configManager.currentConfigForTesting = _dimInfo( events: const [ EventDefinition(eventCode: 'EVENT_TYPE_MISMATCH'), ], tags: const [ TagDefinition(tagName: 'age', tagType: 'int', isRequired: false), ], ); await core.track( 'EVENT_TYPE_MISMATCH', customTags: const {'age': 'bad'}, ); final stored = storage.singleOrNull; final tags = stored?.event.customTags ?? const {}; expect(tags['_sdk_type_error_fields'], contains('age')); await core.dispose(); }); test('metrics/config 定时器会在 tick 时触发任务', () async { final storage = MemoryEventStorage(); late IntervalConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = IntervalConfigManager( config: c, interval: const Duration(milliseconds: 10), ), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); final config = AnalyticsConfig( systemCode: 'TEST_APP', endpointBaseUrl: 'https://example.com', flushInterval: 3600, metricsReportInterval: Duration(milliseconds: 10), ); await core.init(config); await Future.delayed(const Duration(milliseconds: 50)); expect(configManager.fetchCalls, greaterThanOrEqualTo(1)); expect(storage.insertedEvents, isNotEmpty); await core.dispose(); }); }); group('MemoryEventStorage contract', () { Event buildEvent(String type, DateTime createTime) { final ts = createTime.millisecondsSinceEpoch; return Event( systemCode: 'TEST_APP', eventType: type, userInfo: null, clientType: 3, clientTimestamp: ts, timestamp: createTime.toUtc().toIso8601String(), deviceInfo: _testDeviceInfo, eventParams: null, customTags: null, createTime: createTime, ); } test('trimToMaxSize 会删除最旧事件并返回删除数', () async { final storage = MemoryEventStorage(); await storage.init(); final now = DateTime.now(); await storage.insert( buildEvent('E1', now.subtract(const Duration(seconds: 3))), ); await storage.insert( buildEvent('E2', now.subtract(const Duration(seconds: 2))), ); await storage.insert( buildEvent('E3', now.subtract(const Duration(seconds: 1))), ); final trimmed = await storage.trimToMaxSize(2); expect(trimmed, 1); expect(await storage.count(), 2); expect(storage.byId(1), isNull); }); test('deleteExpired 会删除早于 cutoff 的事件', () async { final storage = MemoryEventStorage(); await storage.init(); final now = DateTime.now(); await storage.insert( buildEvent('OLD', now.subtract(const Duration(days: 8))), ); await storage.insert(buildEvent('KEEP', now)); final removed = await storage.deleteExpired( now.subtract(const Duration(days: 7)), ); expect(removed, 1); expect(await storage.count(), 1); expect(storage.singleOrNull?.event.eventType, 'KEEP'); }); test('fetchRecent 按时间降序返回', () async { final storage = MemoryEventStorage(); await storage.init(); final now = DateTime.now(); await storage.insert( buildEvent('OLD', now.subtract(const Duration(seconds: 2))), ); await storage.insert( buildEvent('MID', now.subtract(const Duration(seconds: 1))), ); await storage.insert(buildEvent('NEW', now)); final recent = await storage.fetchRecent(2); expect( recent.map(_eventTypeOfStored).toList(growable: false), ['NEW', 'MID'], ); }); test('updateRetryCount 会同时更新 stored 与 event.retryCount', () async { final storage = MemoryEventStorage(); await storage.init(); final now = DateTime.now(); final id = await storage.insert(buildEvent('RETRY', now)); await storage.updateRetryCount(id, 2); final stored = storage.byId(id); expect(stored, isNotNull); expect(stored!.retryCount, 2); expect(stored.event.retryCount, 2); }); }); group('Integration & performance (mocked)', () { test('断网失败后恢复可补发', () async { final storage = MemoryEventStorage(); late FakeApiClient apiClient; late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: (c) => apiClient = FakeApiClient(c), configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init(_testConfig(enableDebug: false, batchSize: 10)); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); apiClient.exceptionToThrow = const ApiException( message: 'offline', retryable: true, ); await core.track('EVENT_OFFLINE'); await core.flush(force: true); expect(await storage.count(), 1); apiClient.exceptionToThrow = null; await core.flush(force: true); expect(await storage.count(), 0); await core.dispose(); }); test('高频 track 1 万次不会崩溃且数据可入库', () async { final storage = MemoryEventStorage(); late TestConfigManager configManager; final core = AnalyticsCore( storageFactory: () => storage, apiClientFactory: FakeApiClient.new, configManagerFactory: (c) => configManager = TestConfigManager(config: c), deviceInfoCollector: _collectTestDeviceInfo, schedulerFactory: _noopScheduler, ); await core.init( _testConfig( enableDebug: false, batchSize: 20000, maxCacheSize: 20000, ), ); configManager.currentConfigForTesting = _dimInfo(events: _allEventDefinitions); for (var i = 0; i < 10000; i++) { await core.track('EVENT_STRESS'); } expect(await storage.count(), 10000); await core.dispose(); }); }); }