yx_tracking_flutter/example/lib/view_model/demo_view_model.dart

377 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}