import 'dart:async'; import 'package:flutter/material.dart'; import 'package:yx_tracking_flutter/yx_tracking_flutter.dart'; final AnalyticsConfig _defaultConfig = AnalyticsConfig( systemCode: 'SDK-TEST-FLUTTER', endpointBaseUrl: 'http://192.168.2.7:18828', // clientType: 3, // Auto-detected enableDebug: true, batchSize: 30, flushInterval: 30, allowInsecureHttp: true, ); Future main() async { WidgetsFlutterBinding.ensureInitialized(); await Analytics.init(_defaultConfig); Analytics.bindLifecycleObserver(); runApp(const DemoApp()); } class DemoApp extends StatelessWidget { const DemoApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: const DemoPage(), theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true), ); } } class DemoPage extends StatefulWidget { const DemoPage({super.key}); @override State createState() => _DemoPageState(); } class _DemoPageState extends State { int _cacheCount = 0; List _recent = const []; final List _logs = []; final Set _running = {}; bool _verboseStressLogs = false; bool _sdkDebugEnabled = true; final TextEditingController _mockBaseUrlController = TextEditingController(); final ScrollController _logScrollController = ScrollController(); Timer? _pollTimer; late AnalyticsConfig _activeConfig; @override void initState() { super.initState(); _activeConfig = _defaultConfig; _sdkDebugEnabled = _activeConfig.enableDebug; _refreshCount(); _pollTimer = Timer.periodic(const Duration(seconds: 2), (_) { _refreshCount(); }); } @override void dispose() { _pollTimer?.cancel(); _mockBaseUrlController.dispose(); _logScrollController.dispose(); super.dispose(); } bool _isRunning(String key) => _running.contains(key); Future _runAction( String key, String label, Future Function() action, { bool suppressSdkLogs = false, }) async { if (_isRunning(key)) { return; } setState(() { _running.add(key); }); 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: '恢复日志'); } if (mounted) { setState(() { _running.remove(key); }); } await _refreshCount(); } } void _addLog(String message) { if (!mounted) { return; } final now = DateTime.now(); final ts = '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}'; setState(() { _logs.add('[$ts] $message'); if (_logs.length > 200) { _logs.removeRange(0, _logs.length - 200); } }); WidgetsBinding.instance.addPostFrameCallback((_) { if (_logScrollController.hasClients) { _logScrollController.jumpTo( _logScrollController.position.maxScrollExtent, ); } }); } 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; if (!mounted) { return; } setState(() { _cacheCount = count; _recent = recent; }); } Map _requiredParams({ required String page, required String url, required String buttonId, Map? extra, }) { final params = { 'Page': page, 'Url': url, 'ButtonId': buttonId, }; if (extra != null && extra.isNotEmpty) { params.addAll(extra); } return params; } 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, clientType: base.clientType, 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); if (!mounted) { return; } setState(() { _activeConfig = config; _sdkDebugEnabled = config.enableDebug; }); if (reason != null && reason.isNotEmpty) { _addLog('SDK 重新初始化: $reason'); } } void _setSdkDebug(bool enabled, {required String reason}) { if (_sdkDebugEnabled == enabled) { return; } Analytics.setDebug(enabled: enabled); setState(() { _sdkDebugEnabled = enabled; }); _addLog('SDK 日志${enabled ? '开启' : '关闭'}($reason)'); } Future _trackDemoEvent() async { await Analytics.track( 'DEMO_BUTTON_CLICK', eventParams: _requiredParams( page: 'demo', url: 'https://example.com/demo', buttonId: 'demo_btn_01', ), customTags: const {'tenantId': 't1', 'feature': 'demo'}, ); } Future _flushNow() async { await Analytics.flush(force: true); } Future _refreshConfig() async { await Analytics.refreshConfig(force: true); } Future _runStressSequential() async { for (var i = 0; i < 1000; i += 1) { await Analytics.track( 'STRESS_SEQ', eventParams: _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: _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: _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: _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: _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 = _mockBaseUrlController.text.trim(); if (mock.isEmpty) { _addLog('⚠️ 请先填写 5xx/Mock BaseUrl'); 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: _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: _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: _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: _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: '恢复正常配置'); } Widget _buildSection(String title, List children) { return Card( margin: const EdgeInsets.symmetric(vertical: 8), child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 12), ...children, ], ), ), ); } Widget _buildConfigSummary() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Endpoint: ${_activeConfig.endpointBaseUrl}'), Text('systemCode: ${_activeConfig.systemCode}'), Text('allowInsecureHttp: ${_activeConfig.allowInsecureHttp}'), Text('useIsolateStorage: ${_activeConfig.useIsolateStorage}'), Text('maxEventAge: ${_activeConfig.maxEventAge.inSeconds}s'), ], ); } Widget _buildRecentList() { if (_recent.isEmpty) { return const Text('暂无事件'); } return ListView.separated( itemCount: _recent.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, index) { final item = _recent[index]; return ListTile( dense: true, title: Text(item.eventType), subtitle: Text( '${item.createTime.toIso8601String()} · retry=${item.retryCount}', ), ); }, ); } Widget _buildLogList() { if (_logs.isEmpty) { return const Text('暂无日志'); } return ListView.builder( controller: _logScrollController, itemCount: _logs.length, itemBuilder: (context, index) => Text(_logs[index]), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('YX Tracking Demo')), body: ListView( padding: const EdgeInsets.all(16), children: [ Text( '本地缓存事件数:$_cacheCount', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), _buildSection('配置摘要', [_buildConfigSummary()]), _buildSection('基础操作', [ Wrap( spacing: 12, runSpacing: 12, children: [ ElevatedButton( onPressed: _isRunning('track_demo') ? null : () => _runAction( 'track_demo', 'Track Demo Event', _trackDemoEvent, ), child: const Text('Track Demo Event'), ), ElevatedButton( onPressed: _isRunning('flush') ? null : () => _runAction('flush', 'Flush Now', _flushNow), child: Text( _isRunning('flush') ? 'Flushing...' : 'Flush Now', ), ), OutlinedButton( onPressed: _isRunning('refresh_config') ? null : () => _runAction( 'refresh_config', 'Refresh Config', _refreshConfig, ), child: Text( _isRunning('refresh_config') ? 'Refreshing...' : 'Refresh Config', ), ), ], ), const SizedBox(height: 8), const Text( '说明:已对接 SDK-TEST-FLUTTER 系统;若为 HTTP 联调,请保持 allowInsecureHttp=true。', ), ]), _buildSection('压力测试', [ SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('压力测试详细日志'), subtitle: const Text('关闭后仅输出结果与异常'), value: _verboseStressLogs, onChanged: (value) { setState(() { _verboseStressLogs = value; }); }, ), Wrap( spacing: 12, runSpacing: 12, children: [ OutlinedButton( onPressed: _isRunning('stress_seq') ? null : () => _runAction( 'stress_seq', 'Track 1000 (Sequential)', _runStressSequential, suppressSdkLogs: !_verboseStressLogs, ), child: Text( _isRunning('stress_seq') ? 'Testing...' : 'Track 1000 (Sequential)', ), ), OutlinedButton( onPressed: _isRunning('stress_con') ? null : () => _runAction( 'stress_con', 'Track 1000 (Concurrent)', _runStressConcurrent, suppressSdkLogs: !_verboseStressLogs, ), child: Text( _isRunning('stress_con') ? 'Testing...' : 'Track 1000 (Concurrent)', ), ), OutlinedButton( onPressed: _isRunning('continuous') ? null : () => _runAction( 'continuous', 'Continuous Track + Flush', _runContinuousTrackFlush, suppressSdkLogs: !_verboseStressLogs, ), child: Text( _isRunning('continuous') ? 'Running...' : 'Continuous Track + Flush (30s)', ), ), ], ), ]), _buildSection('错误模拟', [ TextField( controller: _mockBaseUrlController, decoration: const InputDecoration( labelText: '5xx/Mock BaseUrl(可选)', hintText: 'http://localhost:8080', ), ), const SizedBox(height: 8), Wrap( spacing: 12, runSpacing: 12, children: [ OutlinedButton( onPressed: _isRunning('invalid_event') ? null : () => _runAction( 'invalid_event', 'Invalid Event (Missing Tag)', _trackInvalidEvent, ), child: const Text('Invalid Event (Missing Tag)'), ), OutlinedButton( onPressed: _isRunning('timeout') ? null : () => _runAction( 'timeout', 'Simulate Network Timeout', _simulateTimeout, ), child: const Text('Simulate Network Timeout'), ), OutlinedButton( onPressed: _isRunning('server_5xx') ? null : () => _runAction( 'server_5xx', 'Simulate Server 5xx', _simulateServerError, ), child: const Text('Simulate Server 5xx'), ), ], ), ]), _buildSection('边界用例', [ Wrap( spacing: 12, runSpacing: 12, children: [ OutlinedButton( onPressed: _isRunning('empty_params') ? null : () => _runAction( 'empty_params', 'Empty Params', _trackEmptyParams, ), child: const Text('Empty Params'), ), OutlinedButton( onPressed: _isRunning('large_payload') ? null : () => _runAction( 'large_payload', 'Large Payload (100KB)', _trackLargePayload, ), child: const Text('Large Payload (100KB)'), ), OutlinedButton( onPressed: _isRunning('special_chars') ? null : () => _runAction( 'special_chars', 'Special Characters', _trackSpecialChars, ), child: const Text('Special Characters'), ), OutlinedButton( onPressed: _isRunning('rapid_init') ? null : () => _runAction( 'rapid_init', 'Rapid Init/Dispose (x5)', _rapidInitDispose, ), child: const Text('Rapid Init/Dispose (x5)'), ), OutlinedButton( onPressed: _isRunning('expiration') ? null : () => _runAction( 'expiration', 'Test Expiration (1s)', _testExpiration, ), child: const Text('Test Expiration (1s)'), ), ], ), ]), _buildSection('状态与日志', [ const Text('最近事件(最多 20 条)'), const SizedBox(height: 8), SizedBox(height: 220, child: _buildRecentList()), const SizedBox(height: 12), const Text('日志输出'), const SizedBox(height: 8), SizedBox(height: 160, child: _buildLogList()), ]), ], ), ); } }