refactor: remove clientType, translate example app, add response logging
Flutter CI / analyze-and-test (push) Has been cancelled Details

This commit is contained in:
Max 2026-02-03 19:26:29 +08:00
parent ee9e6739b5
commit 58bb484cd2
18 changed files with 837 additions and 788 deletions

View File

@ -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,

View File

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

View File

@ -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,
);
}
}

View File

@ -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,
),
);
}
}

View File

@ -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,
),
),
],
),
],
),
);
}
}

View File

@ -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]),
);
}
}

View File

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

View File

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

View File

@ -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() {

View File

@ -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);

View File

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

View File

@ -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),
); );

View File

@ -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,
); );
} }

View File

@ -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,
); );
} }

View File

@ -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),
); );
} }

View File

@ -35,7 +35,6 @@ AnalyticsConfig _config(String baseUrl) {
return AnalyticsConfig( return AnalyticsConfig(
systemCode: 'SYS', systemCode: 'SYS',
endpointBaseUrl: baseUrl, endpointBaseUrl: baseUrl,
clientType: 3,
); );
} }

View File

@ -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,
); );
} }

View File

@ -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);