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 _recent = const []; final List _logs = []; final Set _running = {}; String _mockBaseUrl = ''; Timer? _pollTimer; AnalyticsConfig get activeConfig => _activeConfig; bool get verboseStressLogs => _verboseStressLogs; bool get sdkDebugEnabled => _sdkDebugEnabled; int get cacheCount => _cacheCount; List get recent => _recent; List get logs => List.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 runAction( String key, String label, Future 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 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 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 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 trackDemoEvent() async { await _analytics.track( 'DEMO_BUTTON_CLICK', eventParams: _analytics.requiredParams( page: 'demo', url: 'https://example.com/demo', buttonId: 'demo_btn_01', ), customTags: const {'tenantId': 't1', 'feature': 'demo'}, ); } Future flushNow() => _analytics.flush(force: true); Future refreshConfig() => _analytics.refreshConfig(force: true); Future 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: {'index': i}, ), customTags: const {'feature': 'stress_seq'}, ); } } Future runStressConcurrent() async { Future 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: {'batch': batch, 'index': i}, ), customTags: const {'feature': 'stress_concurrent'}, ); } } final tasks = List>.generate( 10, (index) => trackBatch(index, 100), ); await Future.wait(tasks); } Future 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: { 'ts': DateTime.now().millisecondsSinceEpoch, }, ), customTags: const {'feature': 'continuous'}, ), ); }); final flushTimer = Timer.periodic(const Duration(seconds: 1), (_) { unawaited(_analytics.flush(force: true)); }); while (DateTime.now().isBefore(endAt)) { await Future.delayed(const Duration(milliseconds: 500)); } trackTimer.cancel(); flushTimer.cancel(); } Future trackInvalidEvent() async { await _analytics.track( 'DEMO_BUTTON_CLICK', eventParams: _analytics.requiredParams( page: 'demo_invalid', url: 'https://example.com/invalid', buttonId: 'demo_invalid', ), customTags: const {'tenantId': 't1'}, ); } Future 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 {'case': 'timeout'}, ), ); await _analytics.flush(force: true); await reinitialize(original, reason: '恢复正常配置'); } Future 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 {'case': 'server_5xx'}, ), ); await _analytics.flush(force: true); await reinitialize(original, reason: '恢复正常配置'); } Future trackEmptyParams() async { await _analytics.track('EMPTY_PARAMS_EVENT'); } Future trackLargePayload() async { final payload = List.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: {'payload': payload, 'size': payload.length}, ), ); } Future trackSpecialChars() async { await _analytics.track( 'SPECIAL_空格_😀_!@#', eventParams: _analytics.requiredParams( page: 'special_chars', url: 'https://example.com/special', buttonId: 'special_chars', extra: const {'feature': 'special_chars'}, ), ); } Future rapidInitDispose() async { for (var i = 0; i < 5; i += 1) { await _analytics.dispose(); await _analytics.init(_activeConfig); } } Future 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 {'case': 'expiration'}, ), ); await Future.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 _refreshCount() async { final results = await Future.wait(>[ _analytics.cachedEventCount(), _analytics.cachedRecentEvents(limit: 20), ]); final count = results[0] as int; final recent = results[1] as List; _cacheCount = count; _recent = recent; notifyListeners(); } }