1378 lines
41 KiB
Dart
1378 lines
41 KiB
Dart
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<int, StoredEvent> _store = <int, StoredEvent>{};
|
||
final List<Event> insertedEvents = <Event>[];
|
||
|
||
@override
|
||
Future<void> init() async {}
|
||
|
||
@override
|
||
Future<int> 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<List<StoredEvent>> 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<List<StoredEvent>> 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<void> deleteByIds(List<int> ids) async {
|
||
for (final id in ids) {
|
||
_store.remove(id);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<int> count() async => _store.length;
|
||
|
||
@override
|
||
Future<int> 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<int> deleteExpired(DateTime cutoff) async {
|
||
final before = _store.length;
|
||
_store.removeWhere((_, value) => !value.createTime.isAfter(cutoff));
|
||
return before - _store.length;
|
||
}
|
||
|
||
@override
|
||
Future<void> 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<void> 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<void> _blocker;
|
||
var _blockedOnce = false;
|
||
|
||
@override
|
||
Future<List<StoredEvent>> 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<List<Event>> sentBatches = <List<Event>>[];
|
||
|
||
@override
|
||
Future<void> sendBatch(List<Event> 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<void> init() async {}
|
||
|
||
@override
|
||
Future<void> fetchAndCacheConfig({bool force = false}) async {}
|
||
|
||
@override
|
||
Future<void> forceRefresh() async {}
|
||
|
||
@override
|
||
Future<void> dispose() async {}
|
||
}
|
||
|
||
class InMemoryConfigStorage implements ConfigStorage {
|
||
SystemDimInfo? _info;
|
||
|
||
@override
|
||
Future<void> init() async {}
|
||
|
||
@override
|
||
Future<void> saveSystemDimInfo(SystemDimInfo info) async {
|
||
_info = info;
|
||
}
|
||
|
||
@override
|
||
Future<SystemDimInfo?> loadSystemDimInfo() async => _info;
|
||
|
||
@override
|
||
Future<void> clear() async {
|
||
_info = null;
|
||
}
|
||
|
||
@override
|
||
Future<void> 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<void> init() async {
|
||
initCalls += 1;
|
||
}
|
||
|
||
@override
|
||
Future<void> fetchAndCacheConfig({bool force = false}) async {
|
||
fetchCalls += 1;
|
||
if (force) {
|
||
forceCalls += 1;
|
||
}
|
||
}
|
||
|
||
@override
|
||
Future<void> forceRefresh() async {
|
||
forceCalls += 1;
|
||
}
|
||
|
||
@override
|
||
Future<void> 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<Event?> Function(Event event)? onBefore;
|
||
final FutureOr<void> Function(Event event, SendResult result)? onAfter;
|
||
|
||
int beforeCalls = 0;
|
||
int afterCalls = 0;
|
||
final List<SendResult> afterResults = <SendResult>[];
|
||
|
||
@override
|
||
FutureOr<Event?> beforeSend(Event event) async {
|
||
beforeCalls += 1;
|
||
final handler = onBefore;
|
||
if (handler == null) {
|
||
return event;
|
||
}
|
||
return handler(event);
|
||
}
|
||
|
||
@override
|
||
FutureOr<void> 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<EventDefinition> events = const <EventDefinition>[],
|
||
List<TagDefinition> tags = const <TagDefinition>[],
|
||
SdkStrategy? strategy,
|
||
}) {
|
||
return SystemDimInfo(
|
||
systemInfo: const SystemInfo(raw: <String, dynamic>{}),
|
||
eventDefinitions: events,
|
||
tagDefinitions: tags,
|
||
sdkStrategy: strategy,
|
||
lastFetchedAt: DateTime.now(),
|
||
);
|
||
}
|
||
|
||
Future<DeviceInfo> _collectTestDeviceInfo() async => _testDeviceInfo;
|
||
|
||
Scheduler _noopScheduler(
|
||
Duration interval,
|
||
Future<void> Function() onTick,
|
||
) =>
|
||
NoopScheduler(interval: interval, onTick: onTick);
|
||
|
||
Future<void> _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<void>.delayed(step);
|
||
}
|
||
}
|
||
|
||
String _eventTypeOfStored(StoredEvent stored) => stored.event.eventType;
|
||
|
||
const _allEventDefinitions = <EventDefinition>[
|
||
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<void> _drainMicrotasks() async {
|
||
await Future<void>.delayed(Duration.zero);
|
||
await Future<void>.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>[
|
||
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 <String, dynamic>{};
|
||
|
||
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: <String, EventStrategy>{},
|
||
),
|
||
);
|
||
|
||
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: <String, EventStrategy>{
|
||
'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: <String, EventStrategy>{
|
||
'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<String, dynamic>.from(
|
||
event.customTags ?? const <String, dynamic>{},
|
||
);
|
||
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<String, dynamic>.from(
|
||
event.customTags ?? const <String, dynamic>{},
|
||
);
|
||
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 <String, dynamic>{};
|
||
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>[
|
||
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>[
|
||
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<void>();
|
||
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<void>.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: <AnalyticsInterceptor>[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>[
|
||
EventDefinition(eventCode: 'EVENT_TYPE_MISMATCH'),
|
||
],
|
||
tags: const <TagDefinition>[
|
||
TagDefinition(tagName: 'age', tagType: 'int', isRequired: false),
|
||
],
|
||
);
|
||
|
||
await core.track(
|
||
'EVENT_TYPE_MISMATCH',
|
||
customTags: const <String, Object?>{'age': 'bad'},
|
||
);
|
||
|
||
final stored = storage.singleOrNull;
|
||
final tags = stored?.event.customTags ?? const <String, dynamic>{};
|
||
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<void>.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),
|
||
<String>['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();
|
||
});
|
||
});
|
||
}
|