refactor: remove clientType, translate example app, add response logging
Flutter CI / analyze-and-test (push) Has been cancelled
Details
Flutter CI / analyze-and-test (push) Has been cancelled
Details
This commit is contained in:
parent
ee9e6739b5
commit
58bb484cd2
|
|
@ -96,13 +96,13 @@ class AnalyticsConfig {
|
||||||
final int clientType; // 1=Android, 2=iOS, 3=Flutter
|
final int clientType; // 1=Android, 2=iOS, 3=Flutter
|
||||||
final bool enableDebug;
|
final bool enableDebug;
|
||||||
|
|
||||||
final int batchSize; // 默认 20
|
final int batchSize; // 默认 30
|
||||||
final int flushInterval; // 秒,默认 15
|
final int flushInterval; // 秒,默认 30
|
||||||
final int maxCacheSize; // 默认 5000
|
final int maxCacheSize; // 默认 10000
|
||||||
final int maxRetryCount; // 默认 3
|
final int maxRetryCount; // 默认 3
|
||||||
|
|
||||||
final Duration connectTimeout; // 默认 5s
|
final Duration connectTimeout; // 默认 10s
|
||||||
final Duration readTimeout; // 默认 5s
|
final Duration readTimeout; // 默认 10s
|
||||||
|
|
||||||
const AnalyticsConfig({
|
const AnalyticsConfig({
|
||||||
required this.systemCode,
|
required this.systemCode,
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import 'dart:async';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
import 'view/demo_app.dart';
|
||||||
|
|
||||||
final AnalyticsConfig _defaultConfig = AnalyticsConfig(
|
final AnalyticsConfig _defaultConfig = AnalyticsConfig(
|
||||||
systemCode: 'SDK-TEST-FLUTTER',
|
systemCode: 'SDK-TEST-FLUTTER',
|
||||||
endpointBaseUrl: 'http://192.168.2.7:18828',
|
endpointBaseUrl: 'http://192.168.2.7:18828',
|
||||||
// clientType: 3, // Auto-detected
|
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 30,
|
batchSize: 50,
|
||||||
flushInterval: 30,
|
flushInterval: 30,
|
||||||
allowInsecureHttp: true,
|
allowInsecureHttp: true,
|
||||||
);
|
);
|
||||||
|
|
@ -19,732 +18,5 @@ Future<void> main() async {
|
||||||
await Analytics.init(_defaultConfig);
|
await Analytics.init(_defaultConfig);
|
||||||
Analytics.bindLifecycleObserver();
|
Analytics.bindLifecycleObserver();
|
||||||
|
|
||||||
runApp(const DemoApp());
|
runApp(DemoApp(initialConfig: _defaultConfig));
|
||||||
}
|
|
||||||
|
|
||||||
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<DemoPage> createState() => _DemoPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DemoPageState extends State<DemoPage> {
|
|
||||||
int _cacheCount = 0;
|
|
||||||
List<RecentEventSummary> _recent = const <RecentEventSummary>[];
|
|
||||||
final List<String> _logs = <String>[];
|
|
||||||
final Set<String> _running = <String>{};
|
|
||||||
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<void> _runAction(
|
|
||||||
String key,
|
|
||||||
String label,
|
|
||||||
Future<void> 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<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>;
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_cacheCount = count;
|
|
||||||
_recent = recent;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> _requiredParams({
|
|
||||||
required String page,
|
|
||||||
required String url,
|
|
||||||
required String buttonId,
|
|
||||||
Map<String, dynamic>? extra,
|
|
||||||
}) {
|
|
||||||
final params = <String, dynamic>{
|
|
||||||
'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<void> _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<void> _trackDemoEvent() async {
|
|
||||||
await Analytics.track(
|
|
||||||
'DEMO_BUTTON_CLICK',
|
|
||||||
eventParams: _requiredParams(
|
|
||||||
page: 'demo',
|
|
||||||
url: 'https://example.com/demo',
|
|
||||||
buttonId: 'demo_btn_01',
|
|
||||||
),
|
|
||||||
customTags: TagTemplates.merge([
|
|
||||||
TagTemplates.businessContext(
|
|
||||||
tenantId: 't1',
|
|
||||||
appVersion: '1.0.0',
|
|
||||||
channel: 'internal_test',
|
|
||||||
),
|
|
||||||
TagTemplates.forScreen(screenName: 'DemoPage', featureModule: 'demo'),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _setUserInfo() async {
|
|
||||||
const userInfo = UserInfo(
|
|
||||||
userId: 10086,
|
|
||||||
userName: 'Test User',
|
|
||||||
account: 'test_account',
|
|
||||||
);
|
|
||||||
await Analytics.setUser(userInfo);
|
|
||||||
_addLog('Set user: ${userInfo.toJson()}');
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _flushNow() async {
|
|
||||||
await Analytics.flush(force: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshConfig() async {
|
|
||||||
await Analytics.refreshConfig(force: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _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: <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: _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: _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: _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: _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 = _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 <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: _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: _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: _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: '恢复正常配置');
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSection(String title, List<Widget> children) {
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
...children,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildConfigSummary() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
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: <Widget>[
|
|
||||||
Text(
|
|
||||||
'本地缓存事件数:$_cacheCount',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildSection('配置摘要', <Widget>[_buildConfigSummary()]),
|
|
||||||
_buildSection('基础操作', <Widget>[
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: <Widget>[
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _isRunning('track_demo')
|
|
||||||
? null
|
|
||||||
: () => _runAction(
|
|
||||||
'track_demo',
|
|
||||||
'Track Demo Event',
|
|
||||||
_trackDemoEvent,
|
|
||||||
),
|
|
||||||
child: const Text('Track Demo Event'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _isRunning('set_user')
|
|
||||||
? null
|
|
||||||
: () => _runAction(
|
|
||||||
'set_user',
|
|
||||||
'Set User Info',
|
|
||||||
_setUserInfo,
|
|
||||||
),
|
|
||||||
child: const Text('Set User Info'),
|
|
||||||
),
|
|
||||||
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('压力测试', <Widget>[
|
|
||||||
SwitchListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: const Text('压力测试详细日志'),
|
|
||||||
subtitle: const Text('关闭后仅输出结果与异常'),
|
|
||||||
value: _verboseStressLogs,
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
_verboseStressLogs = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: <Widget>[
|
|
||||||
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('错误模拟', <Widget>[
|
|
||||||
TextField(
|
|
||||||
controller: _mockBaseUrlController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: '5xx/Mock BaseUrl(可选)',
|
|
||||||
hintText: 'http://localhost:8080',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: <Widget>[
|
|
||||||
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('边界用例', <Widget>[
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: <Widget>[
|
|
||||||
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('状态与日志', <Widget>[
|
|
||||||
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()),
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
class AnalyticsService {
|
||||||
|
const AnalyticsService();
|
||||||
|
|
||||||
|
Future<void> init(AnalyticsConfig config) => Analytics.init(config);
|
||||||
|
|
||||||
|
Future<void> dispose() => Analytics.dispose();
|
||||||
|
|
||||||
|
Future<void> flush({bool force = false}) => Analytics.flush(force: force);
|
||||||
|
|
||||||
|
Future<void> refreshConfig({bool force = true}) =>
|
||||||
|
Analytics.refreshConfig(force: force);
|
||||||
|
|
||||||
|
Future<int> cachedEventCount() => Analytics.cachedEventCount();
|
||||||
|
|
||||||
|
Future<List<RecentEventSummary>> cachedRecentEvents({int limit = 20}) =>
|
||||||
|
Analytics.cachedRecentEvents(limit: limit);
|
||||||
|
|
||||||
|
void setDebug({required bool enabled}) => Analytics.setDebug(enabled: enabled);
|
||||||
|
|
||||||
|
Map<String, dynamic> requiredParams({
|
||||||
|
required String page,
|
||||||
|
required String url,
|
||||||
|
required String buttonId,
|
||||||
|
Map<String, dynamic>? extra,
|
||||||
|
}) {
|
||||||
|
final params = <String, dynamic>{
|
||||||
|
'Page': page,
|
||||||
|
'Url': url,
|
||||||
|
'ButtonId': buttonId,
|
||||||
|
};
|
||||||
|
if (extra != null && extra.isNotEmpty) {
|
||||||
|
params.addAll(extra);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> track(
|
||||||
|
String eventType, {
|
||||||
|
Map<String, dynamic>? eventParams,
|
||||||
|
Map<String, dynamic>? customTags,
|
||||||
|
}) {
|
||||||
|
return Analytics.track(
|
||||||
|
eventType,
|
||||||
|
eventParams: eventParams,
|
||||||
|
customTags: customTags,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
import 'demo_page.dart';
|
||||||
|
|
||||||
|
class DemoApp extends StatelessWidget {
|
||||||
|
const DemoApp({super.key, required this.initialConfig});
|
||||||
|
|
||||||
|
final AnalyticsConfig initialConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: DemoPage(initialConfig: initialConfig),
|
||||||
|
theme: ThemeData(
|
||||||
|
colorSchemeSeed: Colors.blue,
|
||||||
|
useMaterial3: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
import '../service/analytics_service.dart';
|
||||||
|
import '../view_model/demo_view_model.dart';
|
||||||
|
import 'widgets.dart';
|
||||||
|
|
||||||
|
class DemoPage extends StatefulWidget {
|
||||||
|
const DemoPage({super.key, required this.initialConfig});
|
||||||
|
|
||||||
|
final AnalyticsConfig initialConfig;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DemoPage> createState() => _DemoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DemoPageState extends State<DemoPage> {
|
||||||
|
late DemoViewModel _viewModel;
|
||||||
|
final TextEditingController _mockBaseUrlController = TextEditingController();
|
||||||
|
final ScrollController _logScrollController = ScrollController();
|
||||||
|
int _lastLogCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_viewModel = DemoViewModel(
|
||||||
|
analytics: const AnalyticsService(),
|
||||||
|
initialConfig: widget.initialConfig,
|
||||||
|
)..init();
|
||||||
|
_mockBaseUrlController.text = _viewModel.mockBaseUrl;
|
||||||
|
_viewModel.addListener(_handleViewModelChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_viewModel.removeListener(_handleViewModelChanged);
|
||||||
|
_viewModel.dispose();
|
||||||
|
_mockBaseUrlController.dispose();
|
||||||
|
_logScrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleViewModelChanged() {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_viewModel.logs.length != _lastLogCount) {
|
||||||
|
_lastLogCount = _viewModel.logs.length;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (_logScrollController.hasClients) {
|
||||||
|
_logScrollController.jumpTo(
|
||||||
|
_logScrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildConfigSummary() {
|
||||||
|
final config = _viewModel.activeConfig;
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Endpoint: ${config.endpointBaseUrl}'),
|
||||||
|
Text('systemCode: ${config.systemCode}'),
|
||||||
|
Text('allowInsecureHttp: ${config.allowInsecureHttp}'),
|
||||||
|
Text('useIsolateStorage: ${config.useIsolateStorage}'),
|
||||||
|
Text('maxEventAge: ${config.maxEventAge.inSeconds}s'),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _actionButton({
|
||||||
|
required String key,
|
||||||
|
required String label,
|
||||||
|
required Future<void> Function() action,
|
||||||
|
bool outlined = true,
|
||||||
|
bool suppressSdkLogs = false,
|
||||||
|
}) {
|
||||||
|
return ActionButton(
|
||||||
|
label: label,
|
||||||
|
outlined: outlined,
|
||||||
|
running: _viewModel.isRunning(key),
|
||||||
|
onPressed: _viewModel.isRunning(key)
|
||||||
|
? null
|
||||||
|
: () => _viewModel.runAction(
|
||||||
|
key,
|
||||||
|
label,
|
||||||
|
action,
|
||||||
|
suppressSdkLogs: suppressSdkLogs,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionButtons(List<Widget> buttons) {
|
||||||
|
return Wrap(spacing: 12, runSpacing: 12, children: buttons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('YX Tracking Demo')),
|
||||||
|
body: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'本地缓存事件数:${_viewModel.cacheCount}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SectionCard(title: '配置摘要', children: <Widget>[_buildConfigSummary()]),
|
||||||
|
SectionCard(
|
||||||
|
title: '基础操作',
|
||||||
|
children: <Widget>[
|
||||||
|
_buildActionButtons(<Widget>[
|
||||||
|
_actionButton(
|
||||||
|
key: 'track_demo',
|
||||||
|
label: 'Track 演示事件',
|
||||||
|
action: _viewModel.trackDemoEvent,
|
||||||
|
outlined: false,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'flush',
|
||||||
|
label: '立即上报',
|
||||||
|
action: _viewModel.flushNow,
|
||||||
|
outlined: false,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'refresh_config',
|
||||||
|
label: '刷新配置',
|
||||||
|
action: _viewModel.refreshConfig,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'说明:已对接 SDK-TEST-FLUTTER 系统;若为 HTTP 联调,请保持 allowInsecureHttp=true。',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: '压力测试',
|
||||||
|
children: <Widget>[
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('压力测试详细日志'),
|
||||||
|
subtitle: const Text('关闭后仅输出结果与异常'),
|
||||||
|
value: _viewModel.verboseStressLogs,
|
||||||
|
onChanged: (value) => _viewModel.setVerboseStressLogs(value),
|
||||||
|
),
|
||||||
|
_buildActionButtons(<Widget>[
|
||||||
|
_actionButton(
|
||||||
|
key: 'stress_seq',
|
||||||
|
label: '串行 Track 1000',
|
||||||
|
action: _viewModel.runStressSequential,
|
||||||
|
suppressSdkLogs: !_viewModel.verboseStressLogs,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'stress_con',
|
||||||
|
label: '并发 Track 1000',
|
||||||
|
action: _viewModel.runStressConcurrent,
|
||||||
|
suppressSdkLogs: !_viewModel.verboseStressLogs,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'continuous',
|
||||||
|
label: '持续 Track + Flush (30秒)',
|
||||||
|
action: _viewModel.runContinuousTrackFlush,
|
||||||
|
suppressSdkLogs: !_viewModel.verboseStressLogs,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: '错误模拟',
|
||||||
|
children: <Widget>[
|
||||||
|
TextField(
|
||||||
|
controller: _mockBaseUrlController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '5xx/Mock BaseUrl(可选)',
|
||||||
|
hintText: 'http://localhost:8080',
|
||||||
|
),
|
||||||
|
onChanged: _viewModel.setMockBaseUrl,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildActionButtons(<Widget>[
|
||||||
|
_actionButton(
|
||||||
|
key: 'invalid_event',
|
||||||
|
label: '非法事件 (缺少标签)',
|
||||||
|
action: _viewModel.trackInvalidEvent,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'timeout',
|
||||||
|
label: '模拟网络超时',
|
||||||
|
action: _viewModel.simulateTimeout,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'server_5xx',
|
||||||
|
label: '模拟服务器 5xx',
|
||||||
|
action: _viewModel.simulateServerError,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: '边界用例',
|
||||||
|
children: <Widget>[
|
||||||
|
_buildActionButtons(<Widget>[
|
||||||
|
_actionButton(
|
||||||
|
key: 'empty_params',
|
||||||
|
label: '空参数',
|
||||||
|
action: _viewModel.trackEmptyParams,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'large_payload',
|
||||||
|
label: '大载荷 (100KB)',
|
||||||
|
action: _viewModel.trackLargePayload,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'special_chars',
|
||||||
|
label: '特殊字符',
|
||||||
|
action: _viewModel.trackSpecialChars,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'rapid_init',
|
||||||
|
label: '快速初始化/销毁 (x5)',
|
||||||
|
action: _viewModel.rapidInitDispose,
|
||||||
|
),
|
||||||
|
_actionButton(
|
||||||
|
key: 'expiration',
|
||||||
|
label: '测试过期 (1秒)',
|
||||||
|
action: _viewModel.testExpiration,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SectionCard(
|
||||||
|
title: '状态与日志',
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('最近事件(最多 20 条)'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: RecentEventList(items: _viewModel.recent),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('日志输出'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
height: 160,
|
||||||
|
child: LogList(
|
||||||
|
logs: _viewModel.logs,
|
||||||
|
controller: _logScrollController,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
class SectionCard extends StatelessWidget {
|
||||||
|
const SectionCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
...children,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ActionButton extends StatelessWidget {
|
||||||
|
const ActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.onPressed,
|
||||||
|
this.outlined = true,
|
||||||
|
this.running = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool outlined;
|
||||||
|
final bool running;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final child = Text(running ? '$label...' : label);
|
||||||
|
if (outlined) {
|
||||||
|
return OutlinedButton(onPressed: onPressed, child: child);
|
||||||
|
}
|
||||||
|
return ElevatedButton(onPressed: onPressed, child: child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecentEventList extends StatelessWidget {
|
||||||
|
const RecentEventList({super.key, required this.items});
|
||||||
|
|
||||||
|
final List<RecentEventSummary> items;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (items.isEmpty) {
|
||||||
|
return const Text('暂无事件');
|
||||||
|
}
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: items.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = items[index];
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(item.eventType),
|
||||||
|
subtitle: Text(
|
||||||
|
'${item.createTime.toIso8601String()} · retry=${item.retryCount}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogList extends StatelessWidget {
|
||||||
|
const LogList({super.key, required this.logs, required this.controller});
|
||||||
|
|
||||||
|
final List<String> logs;
|
||||||
|
final ScrollController controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (logs.isEmpty) {
|
||||||
|
return const Text('暂无日志');
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
controller: controller,
|
||||||
|
itemCount: logs.length,
|
||||||
|
itemBuilder: (context, index) => Text(logs[index]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,376 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:example/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Demo 页面基础元素可渲染', (WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(const DemoApp());
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(find.text('YX Tracking Demo'), findsOneWidget);
|
|
||||||
expect(find.textContaining('本地缓存事件数'), findsOneWidget);
|
|
||||||
expect(find.text('Track Demo Event'), findsOneWidget);
|
|
||||||
expect(find.text('Flush Now'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -7,21 +7,20 @@ class AnalyticsConfig {
|
||||||
AnalyticsConfig({
|
AnalyticsConfig({
|
||||||
required this.systemCode,
|
required this.systemCode,
|
||||||
required this.endpointBaseUrl,
|
required this.endpointBaseUrl,
|
||||||
int? clientType,
|
|
||||||
this.enableDebug = false,
|
this.enableDebug = false,
|
||||||
this.batchSize = 20,
|
this.batchSize = 30,
|
||||||
this.flushInterval = 15,
|
this.flushInterval = 30,
|
||||||
this.maxCacheSize = 5000,
|
this.maxCacheSize = 10000,
|
||||||
this.maxRetryCount = 3,
|
this.maxRetryCount = 3,
|
||||||
this.connectTimeout = const Duration(seconds: 5),
|
this.connectTimeout = const Duration(seconds: 10),
|
||||||
this.readTimeout = const Duration(seconds: 5),
|
this.readTimeout = const Duration(seconds: 10),
|
||||||
this.maxEventAge = const Duration(days: 7),
|
this.maxEventAge = const Duration(days: 7),
|
||||||
this.useIsolateStorage = true,
|
this.useIsolateStorage = true,
|
||||||
this.allowInsecureHttp = false,
|
this.allowInsecureHttp = false,
|
||||||
this.enableMetrics = true,
|
this.enableMetrics = true,
|
||||||
this.metricsReportInterval = const Duration(minutes: 10),
|
this.metricsReportInterval = const Duration(minutes: 10),
|
||||||
this.blockOnValidationError = false,
|
this.blockOnValidationError = false,
|
||||||
}) : clientType = clientType ?? _detectClientType();
|
}) : clientType = _detectClientType();
|
||||||
|
|
||||||
/// 自动探测客户端类型。
|
/// 自动探测客户端类型。
|
||||||
static int _detectClientType() {
|
static int _detectClientType() {
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,8 @@ class ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
final size = events.length;
|
final size = events.length;
|
||||||
Logger.info('批量上报成功: size=$size, status=$statusCode');
|
Logger.info(
|
||||||
|
'批量上报成功: size=$size, status=$statusCode, response=${response.data}');
|
||||||
} on DioException catch (e, st) {
|
} on DioException catch (e, st) {
|
||||||
final apiException = _mapDioException(e);
|
final apiException = _mapDioException(e);
|
||||||
Logger.error('批量上报异常: ${apiException.message}', apiException, st);
|
Logger.error('批量上报异常: ${apiException.message}', apiException, st);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:yx_tracking_flutter/src/config/analytics_config.dart';
|
||||||
AnalyticsConfig _base({
|
AnalyticsConfig _base({
|
||||||
String? systemCode,
|
String? systemCode,
|
||||||
String? endpointBaseUrl,
|
String? endpointBaseUrl,
|
||||||
int? clientType,
|
|
||||||
int? batchSize,
|
int? batchSize,
|
||||||
int? flushInterval,
|
int? flushInterval,
|
||||||
int? maxCacheSize,
|
int? maxCacheSize,
|
||||||
|
|
@ -20,19 +19,17 @@ AnalyticsConfig _base({
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: systemCode ?? 'SYS',
|
systemCode: systemCode ?? 'SYS',
|
||||||
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com',
|
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com',
|
||||||
clientType: clientType ?? 3,
|
batchSize: batchSize ?? 30,
|
||||||
batchSize: batchSize ?? 20,
|
flushInterval: flushInterval ?? 30,
|
||||||
flushInterval: flushInterval ?? 15,
|
|
||||||
maxCacheSize: maxCacheSize ?? 100,
|
maxCacheSize: maxCacheSize ?? 100,
|
||||||
maxRetryCount: maxRetryCount ?? 3,
|
maxRetryCount: maxRetryCount ?? 3,
|
||||||
connectTimeout: connectTimeout ?? const Duration(seconds: 1),
|
connectTimeout: connectTimeout ?? const Duration(seconds: 10),
|
||||||
readTimeout: readTimeout ?? const Duration(seconds: 1),
|
readTimeout: readTimeout ?? const Duration(seconds: 10),
|
||||||
maxEventAge: maxEventAge ?? const Duration(days: 7),
|
maxEventAge: maxEventAge ?? const Duration(days: 7),
|
||||||
useIsolateStorage: useIsolateStorage ?? false,
|
useIsolateStorage: useIsolateStorage ?? false,
|
||||||
allowInsecureHttp: allowInsecureHttp ?? false,
|
allowInsecureHttp: allowInsecureHttp ?? false,
|
||||||
enableMetrics: enableMetrics ?? true,
|
enableMetrics: enableMetrics ?? true,
|
||||||
metricsReportInterval:
|
metricsReportInterval: metricsReportInterval ?? const Duration(minutes: 1),
|
||||||
metricsReportInterval ?? const Duration(minutes: 1),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,17 +62,13 @@ void main() {
|
||||||
|
|
||||||
test('允许 http 时不抛错', () {
|
test('允许 http 时不抛错', () {
|
||||||
expect(
|
expect(
|
||||||
() =>
|
() => _base(
|
||||||
_base(endpointBaseUrl: 'http://example.com', allowInsecureHttp: true)
|
endpointBaseUrl: 'http://example.com', allowInsecureHttp: true)
|
||||||
.validate(),
|
.validate(),
|
||||||
returnsNormally,
|
returnsNormally,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('clientType 必须为正数', () {
|
|
||||||
expect(() => _base(clientType: 0).validate(), throwsArgumentError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('batchSize 必须为正数', () {
|
test('batchSize 必须为正数', () {
|
||||||
expect(() => _base(batchSize: 0).validate(), throwsArgumentError);
|
expect(() => _base(batchSize: 0).validate(), throwsArgumentError);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -291,7 +291,6 @@ AnalyticsConfig _testConfig({
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableDebug: enableDebug,
|
enableDebug: enableDebug,
|
||||||
batchSize: batchSize,
|
batchSize: batchSize,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
|
|
@ -754,7 +753,6 @@ void main() {
|
||||||
final config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 10,
|
batchSize: 10,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
|
|
@ -882,7 +880,6 @@ void main() {
|
||||||
final config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
|
|
@ -1209,7 +1206,6 @@ void main() {
|
||||||
final config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
metricsReportInterval: Duration(milliseconds: 10),
|
metricsReportInterval: Duration(milliseconds: 10),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'SYS',
|
systemCode: 'SYS',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,6 @@ AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,6 @@ AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableDebug: true,
|
|
||||||
metricsReportInterval: Duration(days: 1),
|
metricsReportInterval: Duration(days: 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ AnalyticsConfig _config(String baseUrl) {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'SYS',
|
systemCode: 'SYS',
|
||||||
endpointBaseUrl: baseUrl,
|
endpointBaseUrl: baseUrl,
|
||||||
clientType: 3,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ void main() {
|
||||||
final config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config.validate, returnsNormally);
|
expect(config.validate, returnsNormally);
|
||||||
|
|
@ -18,7 +17,6 @@ void main() {
|
||||||
final config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'http://example.com',
|
endpointBaseUrl: 'http://example.com',
|
||||||
clientType: 3,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(config.validate, throwsArgumentError);
|
expect(config.validate, throwsArgumentError);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue