377 lines
11 KiB
Dart
377 lines
11 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||
|
||
import '../service/analytics_service.dart';
|
||
|
||
class DemoViewModel extends ChangeNotifier {
|
||
DemoViewModel({
|
||
required AnalyticsService analytics,
|
||
required AnalyticsConfig initialConfig,
|
||
}) : _analytics = analytics,
|
||
_activeConfig = initialConfig,
|
||
_initialConfig = initialConfig {
|
||
_sdkDebugEnabled = initialConfig.enableDebug;
|
||
}
|
||
|
||
final AnalyticsService _analytics;
|
||
final AnalyticsConfig _initialConfig;
|
||
AnalyticsConfig _activeConfig;
|
||
bool _verboseStressLogs = false;
|
||
bool _sdkDebugEnabled = true;
|
||
int _cacheCount = 0;
|
||
List<RecentEventSummary> _recent = const <RecentEventSummary>[];
|
||
final List<String> _logs = <String>[];
|
||
final Set<String> _running = <String>{};
|
||
String _mockBaseUrl = '';
|
||
Timer? _pollTimer;
|
||
|
||
AnalyticsConfig get activeConfig => _activeConfig;
|
||
bool get verboseStressLogs => _verboseStressLogs;
|
||
bool get sdkDebugEnabled => _sdkDebugEnabled;
|
||
int get cacheCount => _cacheCount;
|
||
List<RecentEventSummary> get recent => _recent;
|
||
List<String> get logs => List<String>.unmodifiable(_logs);
|
||
String get mockBaseUrl => _mockBaseUrl;
|
||
|
||
void init() {
|
||
_refreshCount();
|
||
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||
_refreshCount();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_pollTimer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
bool isRunning(String key) => _running.contains(key);
|
||
|
||
void setVerboseStressLogs(bool value) {
|
||
if (_verboseStressLogs == value) {
|
||
return;
|
||
}
|
||
_verboseStressLogs = value;
|
||
notifyListeners();
|
||
}
|
||
|
||
void setMockBaseUrl(String value) {
|
||
if (_mockBaseUrl == value) {
|
||
return;
|
||
}
|
||
_mockBaseUrl = value;
|
||
}
|
||
|
||
Future<void> runAction(
|
||
String key,
|
||
String label,
|
||
Future<void> Function() action, {
|
||
bool suppressSdkLogs = false,
|
||
}) async {
|
||
if (isRunning(key)) {
|
||
return;
|
||
}
|
||
_running.add(key);
|
||
notifyListeners();
|
||
|
||
final start = DateTime.now();
|
||
_addLog('▶ $label');
|
||
|
||
final previousDebug = _sdkDebugEnabled;
|
||
if (suppressSdkLogs) {
|
||
_setSdkDebug(false, reason: '压力测试静默');
|
||
}
|
||
|
||
try {
|
||
await action();
|
||
final elapsed = DateTime.now().difference(start).inMilliseconds;
|
||
_addLog('✅ $label (${elapsed}ms)');
|
||
} on Object catch (e) {
|
||
_addLog('❌ $label: $e');
|
||
} finally {
|
||
if (suppressSdkLogs) {
|
||
_setSdkDebug(previousDebug, reason: '恢复日志');
|
||
}
|
||
_running.remove(key);
|
||
notifyListeners();
|
||
await _refreshCount();
|
||
}
|
||
}
|
||
|
||
Future<void> refreshCount() => _refreshCount();
|
||
|
||
AnalyticsConfig buildConfig({
|
||
String? endpointBaseUrl,
|
||
Duration? maxEventAge,
|
||
bool? allowInsecureHttp,
|
||
bool? useIsolateStorage,
|
||
Duration? connectTimeout,
|
||
Duration? readTimeout,
|
||
}) {
|
||
final base = _activeConfig;
|
||
return AnalyticsConfig(
|
||
systemCode: base.systemCode,
|
||
endpointBaseUrl: endpointBaseUrl ?? base.endpointBaseUrl,
|
||
enableDebug: base.enableDebug,
|
||
batchSize: base.batchSize,
|
||
flushInterval: base.flushInterval,
|
||
maxCacheSize: base.maxCacheSize,
|
||
maxRetryCount: base.maxRetryCount,
|
||
connectTimeout: connectTimeout ?? base.connectTimeout,
|
||
readTimeout: readTimeout ?? base.readTimeout,
|
||
maxEventAge: maxEventAge ?? base.maxEventAge,
|
||
useIsolateStorage: useIsolateStorage ?? base.useIsolateStorage,
|
||
allowInsecureHttp: allowInsecureHttp ?? base.allowInsecureHttp,
|
||
enableMetrics: base.enableMetrics,
|
||
metricsReportInterval: base.metricsReportInterval,
|
||
blockOnValidationError: base.blockOnValidationError,
|
||
);
|
||
}
|
||
|
||
Future<void> reinitialize(AnalyticsConfig config, {String? reason}) async {
|
||
await _analytics.init(config);
|
||
_activeConfig = config;
|
||
_sdkDebugEnabled = config.enableDebug;
|
||
if (reason != null && reason.isNotEmpty) {
|
||
_addLog('SDK 重新初始化: $reason');
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> resetToInitial() => reinitialize(_initialConfig);
|
||
|
||
void _setSdkDebug(bool enabled, {required String reason}) {
|
||
if (_sdkDebugEnabled == enabled) {
|
||
return;
|
||
}
|
||
_analytics.setDebug(enabled: enabled);
|
||
_sdkDebugEnabled = enabled;
|
||
_addLog('SDK 日志${enabled ? '开启' : '关闭'}($reason)');
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> trackDemoEvent() async {
|
||
await _analytics.track(
|
||
'DEMO_BUTTON_CLICK',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'demo',
|
||
url: 'https://example.com/demo',
|
||
buttonId: 'demo_btn_01',
|
||
),
|
||
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||
);
|
||
}
|
||
|
||
Future<void> flushNow() => _analytics.flush(force: true);
|
||
|
||
Future<void> refreshConfig() => _analytics.refreshConfig(force: true);
|
||
|
||
Future<void> runStressSequential() async {
|
||
for (var i = 0; i < 1000; i += 1) {
|
||
await _analytics.track(
|
||
'STRESS_SEQ',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'stress_seq',
|
||
url: 'https://example.com/stress',
|
||
buttonId: 'stress_seq',
|
||
extra: <String, dynamic>{'index': i},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'stress_seq'},
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> runStressConcurrent() async {
|
||
Future<void> trackBatch(int batch, int count) async {
|
||
for (var i = 0; i < count; i += 1) {
|
||
await _analytics.track(
|
||
'STRESS_CONCURRENT',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'stress_concurrent',
|
||
url: 'https://example.com/stress',
|
||
buttonId: 'stress_concurrent',
|
||
extra: <String, dynamic>{'batch': batch, 'index': i},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'stress_concurrent'},
|
||
);
|
||
}
|
||
}
|
||
|
||
final tasks = List<Future<void>>.generate(
|
||
10,
|
||
(index) => trackBatch(index, 100),
|
||
);
|
||
await Future.wait(tasks);
|
||
}
|
||
|
||
Future<void> runContinuousTrackFlush() async {
|
||
final endAt = DateTime.now().add(const Duration(seconds: 30));
|
||
final trackTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||
unawaited(
|
||
_analytics.track(
|
||
'CONTINUOUS_EVENT',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'continuous',
|
||
url: 'https://example.com/continuous',
|
||
buttonId: 'continuous',
|
||
extra: <String, dynamic>{
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'continuous'},
|
||
),
|
||
);
|
||
});
|
||
final flushTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||
unawaited(_analytics.flush(force: true));
|
||
});
|
||
|
||
while (DateTime.now().isBefore(endAt)) {
|
||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||
}
|
||
|
||
trackTimer.cancel();
|
||
flushTimer.cancel();
|
||
}
|
||
|
||
Future<void> trackInvalidEvent() async {
|
||
await _analytics.track(
|
||
'DEMO_BUTTON_CLICK',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'demo_invalid',
|
||
url: 'https://example.com/invalid',
|
||
buttonId: 'demo_invalid',
|
||
),
|
||
customTags: const <String, dynamic>{'tenantId': 't1'},
|
||
);
|
||
}
|
||
|
||
Future<void> simulateTimeout() async {
|
||
final original = _activeConfig;
|
||
final tempConfig = buildConfig(
|
||
endpointBaseUrl: 'http://10.255.255.1:18828',
|
||
allowInsecureHttp: true,
|
||
connectTimeout: const Duration(seconds: 1),
|
||
readTimeout: const Duration(seconds: 1),
|
||
);
|
||
await reinitialize(tempConfig, reason: '网络超时模拟');
|
||
await _analytics.track(
|
||
'EVENT_TIMEOUT_TEST',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'timeout',
|
||
url: 'https://example.com/timeout',
|
||
buttonId: 'timeout',
|
||
extra: const <String, dynamic>{'case': 'timeout'},
|
||
),
|
||
);
|
||
await _analytics.flush(force: true);
|
||
await reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
Future<void> simulateServerError() async {
|
||
final mock = _mockBaseUrl.trim();
|
||
if (mock.isEmpty) {
|
||
_addLog('⚠️ 请先填写 5xx/Mock BaseUrl');
|
||
notifyListeners();
|
||
return;
|
||
}
|
||
final original = _activeConfig;
|
||
final tempConfig = buildConfig(
|
||
endpointBaseUrl: mock,
|
||
allowInsecureHttp: mock.startsWith('http://'),
|
||
);
|
||
await reinitialize(tempConfig, reason: '5xx 模拟');
|
||
await _analytics.track(
|
||
'EVENT_5XX_TEST',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'server_5xx',
|
||
url: 'https://example.com/5xx',
|
||
buttonId: 'server_5xx',
|
||
extra: const <String, dynamic>{'case': 'server_5xx'},
|
||
),
|
||
);
|
||
await _analytics.flush(force: true);
|
||
await reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
Future<void> trackEmptyParams() async {
|
||
await _analytics.track('EMPTY_PARAMS_EVENT');
|
||
}
|
||
|
||
Future<void> trackLargePayload() async {
|
||
final payload = List<String>.filled(100 * 1024, 'a').join();
|
||
await _analytics.track(
|
||
'LARGE_PAYLOAD_EVENT',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'large_payload',
|
||
url: 'https://example.com/large',
|
||
buttonId: 'large_payload',
|
||
extra: <String, dynamic>{'payload': payload, 'size': payload.length},
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> trackSpecialChars() async {
|
||
await _analytics.track(
|
||
'SPECIAL_空格_😀_!@#',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'special_chars',
|
||
url: 'https://example.com/special',
|
||
buttonId: 'special_chars',
|
||
extra: const <String, dynamic>{'feature': 'special_chars'},
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> rapidInitDispose() async {
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await _analytics.dispose();
|
||
await _analytics.init(_activeConfig);
|
||
}
|
||
}
|
||
|
||
Future<void> testExpiration() async {
|
||
final original = _activeConfig;
|
||
final tempConfig = buildConfig(maxEventAge: const Duration(seconds: 1));
|
||
await reinitialize(tempConfig, reason: '过期清理测试');
|
||
await _analytics.track(
|
||
'EXPIRATION_EVENT',
|
||
eventParams: _analytics.requiredParams(
|
||
page: 'expiration',
|
||
url: 'https://example.com/expiration',
|
||
buttonId: 'expiration',
|
||
extra: const <String, dynamic>{'case': 'expiration'},
|
||
),
|
||
);
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
await _analytics.flush(force: true);
|
||
await reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
void _addLog(String message) {
|
||
final now = DateTime.now();
|
||
final ts =
|
||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||
_logs.add('[$ts] $message');
|
||
if (_logs.length > 200) {
|
||
_logs.removeRange(0, _logs.length - 200);
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> _refreshCount() async {
|
||
final results = await Future.wait(<Future<Object>>[
|
||
_analytics.cachedEventCount(),
|
||
_analytics.cachedRecentEvents(limit: 20),
|
||
]);
|
||
final count = results[0] as int;
|
||
final recent = results[1] as List<RecentEventSummary>;
|
||
_cacheCount = count;
|
||
_recent = recent;
|
||
notifyListeners();
|
||
}
|
||
}
|