feat: enhance example app, config & client type
- feat(example): add stress testing, error simulation & edge case UI - feat(config): add ClientType enum with auto-detection - refactor(config): centralize URL management & fix 404/400 errors - test: update tests for non-const config & verify functionality
This commit is contained in:
parent
29c60bbc48
commit
3277efd47c
|
|
@ -47,7 +47,9 @@
|
||||||
"screenResolution": "string"
|
"screenResolution": "string"
|
||||||
},
|
},
|
||||||
"eventParams": {
|
"eventParams": {
|
||||||
// 事件上下文
|
"Page": "string",
|
||||||
|
"Url": "string",
|
||||||
|
"ButtonId": "string"
|
||||||
},
|
},
|
||||||
"customTags": {
|
"customTags": {
|
||||||
// 业务维度
|
// 业务维度
|
||||||
|
|
@ -90,7 +92,7 @@ class Analytics {
|
||||||
```dart
|
```dart
|
||||||
class AnalyticsConfig {
|
class AnalyticsConfig {
|
||||||
final String systemCode;
|
final String systemCode;
|
||||||
final String endpointBaseUrl; // https://host/api/ExternalEventlogs
|
final String endpointBaseUrl; // https://host(仅基础 host)
|
||||||
final int clientType; // 1=Android, 2=iOS, 3=Flutter
|
final int clientType; // 1=Android, 2=iOS, 3=Flutter
|
||||||
final bool enableDebug;
|
final bool enableDebug;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
| 项目 | 值 |
|
| 项目 | 值 |
|
||||||
|------|---|
|
|------|---|
|
||||||
| **API 基础地址** | `http://192.168.2.7:18828/api/ExternalEventlogs` |
|
| **API 基础地址** | `http://192.168.2.7:18828` |
|
||||||
| **系统标识** | `SDK-TEST-FLUTTER` |
|
| **系统标识** | `SDK-TEST-FLUTTER` |
|
||||||
| **系统名称** | Flutter SDK测试 |
|
| **系统名称** | Flutter SDK测试 |
|
||||||
| **认证方式** | 无需认证 |
|
| **认证方式** | 无需认证 |
|
||||||
|
|
@ -47,9 +47,9 @@
|
||||||
await Analytics.init(
|
await Analytics.init(
|
||||||
const AnalyticsConfig(
|
const AnalyticsConfig(
|
||||||
- systemCode: 'DEMO_APP',
|
- systemCode: 'DEMO_APP',
|
||||||
- endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
- endpointBaseUrl: 'https://example.com',
|
||||||
+ systemCode: 'SDK-TEST-FLUTTER',
|
+ systemCode: 'SDK-TEST-FLUTTER',
|
||||||
+ endpointBaseUrl: 'http://192.168.2.7:18828/api/ExternalEventlogs',
|
+ endpointBaseUrl: 'http://192.168.2.7:18828',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 5,
|
batchSize: 5,
|
||||||
|
|
@ -154,7 +154,11 @@ Content-Type: application/json
|
||||||
"model": "MacBook Pro",
|
"model": "MacBook Pro",
|
||||||
"screenResolution": "1920x1080"
|
"screenResolution": "1920x1080"
|
||||||
},
|
},
|
||||||
"eventParams": {"page": "demo"},
|
"eventParams": {
|
||||||
|
"Page": "demo",
|
||||||
|
"Url": "https://example.com/demo",
|
||||||
|
"ButtonId": "demo_btn_01"
|
||||||
|
},
|
||||||
"customTags": {"tenantId": "t1", "feature": "demo"}
|
"customTags": {"tenantId": "t1", "feature": "demo"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ Future<void> bootstrapAnalytics() async {
|
||||||
await Analytics.init(
|
await Analytics.init(
|
||||||
const AnalyticsConfig(
|
const AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'https://your-host/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://your-host',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
),
|
),
|
||||||
|
|
@ -47,7 +47,11 @@ Future<void> bootstrapAnalytics() async {
|
||||||
```dart
|
```dart
|
||||||
await Analytics.track(
|
await Analytics.track(
|
||||||
'PAGE_VIEW',
|
'PAGE_VIEW',
|
||||||
eventParams: const <String, dynamic>{'page': 'home'},
|
eventParams: const <String, dynamic>{
|
||||||
|
'Page': 'home',
|
||||||
|
'Url': 'https://example.com/home',
|
||||||
|
'ButtonId': 'page_view',
|
||||||
|
},
|
||||||
customTags: const <String, dynamic>{'tenantId': 't1'},
|
customTags: const <String, dynamic>{'tenantId': 't1'},
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
@ -61,6 +65,7 @@ await Analytics.flush(force: true);
|
||||||
## 关键配置项(AnalyticsConfig)
|
## 关键配置项(AnalyticsConfig)
|
||||||
|
|
||||||
除了文档中的 Phase 1 配置项,还新增了以下能力配置:
|
除了文档中的 Phase 1 配置项,还新增了以下能力配置:
|
||||||
|
- `endpointBaseUrl`:仅填写基础 host(如 `https://your-host`),SDK 会自动拼接 `/api/ExternalEventlogs/*` 路径
|
||||||
- `useIsolateStorage`: 是否使用 Isolate 进行存储操作(默认 `true`,强烈建议在 Flutter 环境开启)
|
- `useIsolateStorage`: 是否使用 Isolate 进行存储操作(默认 `true`,强烈建议在 Flutter 环境开启)
|
||||||
- `allowInsecureHttp`:是否允许使用 HTTP(默认 `false`,仅用于开发/测试环境)
|
- `allowInsecureHttp`:是否允许使用 HTTP(默认 `false`,仅用于开发/测试环境)
|
||||||
- `enableMetrics`:是否启用 SDK 自监控指标(默认 `true`)
|
- `enableMetrics`:是否启用 SDK 自监控指标(默认 `true`)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
# Example App Comprehensive Test Plan
|
||||||
|
# 示例应用综合测试规划
|
||||||
|
|
||||||
|
## Goal / 目标
|
||||||
|
Ensure the SDK works reliably in production and any SDK errors do not crash or impact the host app.
|
||||||
|
确保 SDK 在生产环境稳定运行,任何 SDK 错误都不会导致宿主应用崩溃或影响正常使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目现状与差距分析(基于当前实现)
|
||||||
|
1. **Example App 已具备基础入口**:`Track/Flush/Refresh Config`,支持缓存条数与最近事件列表。
|
||||||
|
2. **配置拉取参数为 `systemCode`**,需后端接口一致;联调地址为 HTTP 时必须开启 `allowInsecureHttp`。
|
||||||
|
3. **生命周期自动 flush 已实现**,但 Example App 需要绑定 `Analytics.bindLifecycleObserver()`。
|
||||||
|
4. **Isolate 存储默认启用**,并支持失败降级到 sqflite;Example App 需补充可验证路径(或增加调试开关)。
|
||||||
|
5. **Validator 行为依赖后端配置**(必填 tag/事件定义需在后台配置)。
|
||||||
|
|
||||||
|
## Test Categories / 测试分类
|
||||||
|
|
||||||
|
### 1. Stress Testing / 压力测试
|
||||||
|
| Feature / 功能 | Description / 描述 |
|
||||||
|
|---|---|
|
||||||
|
| **Batch Track (1000 events)** | Rapidly track 1000 events to test storage throughput. / 快速记录 1000 条事件,测试存储吞吐量。 |
|
||||||
|
| **Concurrent Track** | Fire multiple `Future`s concurrently (same isolate). / 同一 isolate 内并发触发多个 Future。 |
|
||||||
|
| **Continuous Flush** | Repeatedly flush while events are still being added. / 在持续添加事件的同时反复 flush。 |
|
||||||
|
|
||||||
|
### 2. Error Handling / 错误处理
|
||||||
|
| Feature / 功能 | Description / 描述 |
|
||||||
|
|---|---|
|
||||||
|
| **Network Failure Simulation** | Disconnect network and track events, verify queue persists and retries. / 断网状态下追踪事件,验证队列是否持久化并可重试。 |
|
||||||
|
| **Server Error (5xx) Simulation** | Point to an endpoint that returns 500 errors. / 指向返回 500 的端点。 |
|
||||||
|
| **Invalid Payload Test** | Track events missing required tags (requires backend config). / 追踪缺少必填标签的事件(依赖后台配置)。 |
|
||||||
|
|
||||||
|
### 3. Edge Cases / 边界情况
|
||||||
|
| Feature / 功能 | Description / 描述 |
|
||||||
|
|---|---|
|
||||||
|
| **Empty Event Params** | Track event with no `eventParams`. / 追踪不带 `eventParams` 的事件。 |
|
||||||
|
| **Very Large Payload** | Track event with a very large JSON payload (e.g., 100KB). / 追踪一个超大 JSON 负载的事件 (例如 100KB)。 |
|
||||||
|
| **Special Characters** | Track event with special characters in `eventType` or params. / 在 `eventType` 或参数中使用特殊字符。 |
|
||||||
|
| **Rapid Init/Dispose** | Initialize and dispose SDK multiple times quickly. / 快速多次初始化和释放 SDK。 |
|
||||||
|
| **Background/Foreground Transitions** | Track events while app is backgrounded. / 在应用后台时追踪事件。 |
|
||||||
|
| **Event Expiration** | Insert old events and verify cleanup. / 插入过期事件并验证清理。 |
|
||||||
|
|
||||||
|
### 4. SDK Robustness (Isolation) / SDK 健壮性(隔离性)
|
||||||
|
| Feature / 功能 | Description / 描述 |
|
||||||
|
|---|---|
|
||||||
|
| **Exception Swallowing** | Verify SDK exceptions are caught and logged, not propagated to app. / 验证 SDK 异常被捕获并记录,而不是传递给应用。 |
|
||||||
|
| **Timeout Handling** | Verify network timeouts don't freeze the app. / 验证网络超时不会冻结应用。 |
|
||||||
|
| **Fallback Storage** | Force Isolate init failure and verify fallback. / 人为触发 Isolate 初始化失败并验证回退。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed UI Structure / 建议的 UI 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
DemoApp
|
||||||
|
├── Basic Actions / 基本操作
|
||||||
|
│ ├── Track Demo Event
|
||||||
|
│ ├── Flush Now
|
||||||
|
│ └── Refresh Config
|
||||||
|
│
|
||||||
|
├── Stress Tests / 压力测试
|
||||||
|
│ ├── Track 1000 Events (Concurrent)
|
||||||
|
│ ├── Track 5000 Events (Sequential)
|
||||||
|
│ └── Continuous Track + Flush
|
||||||
|
│
|
||||||
|
├── Error Simulation / 错误模拟
|
||||||
|
│ ├── Track Invalid Event (missing fields)
|
||||||
|
│ ├── Simulate Network Timeout
|
||||||
|
│ └── Simulate Server 5xx Error
|
||||||
|
│
|
||||||
|
├── Edge Cases / 边界情况
|
||||||
|
│ ├── Track Empty Params Event
|
||||||
|
│ ├── Track Large Payload Event
|
||||||
|
│ └── Rapid Init/Dispose Cycle
|
||||||
|
│
|
||||||
|
└── Status & Logs / 状态与日志
|
||||||
|
├── Cache Count
|
||||||
|
├── Recent Events List
|
||||||
|
└── Real-time Log Output
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 具体测试用例(结合当前 SDK)
|
||||||
|
|
||||||
|
### A. 基础联通(必做)
|
||||||
|
1. **初始化与配置**
|
||||||
|
- 配置 `allowInsecureHttp: true`(仅联调)。
|
||||||
|
- `systemCode` 与后端配置一致。
|
||||||
|
- 期望:初始化成功,不抛异常。
|
||||||
|
2. **Track + Flush**
|
||||||
|
- 连续点击 Track 5 次,触发 flush。
|
||||||
|
- 期望:缓存减少,后端收到事件。
|
||||||
|
3. **Refresh Config**
|
||||||
|
- 点击刷新配置。
|
||||||
|
- 期望:ConfigManager 拉取成功或日志提示跳过(未过期)。
|
||||||
|
|
||||||
|
### B. 压力与稳定性
|
||||||
|
1. **1000 条批量写入**
|
||||||
|
- 一键触发批量 track。
|
||||||
|
- 期望:无崩溃,无明显卡顿;缓存数正确。
|
||||||
|
2. **并发写入**
|
||||||
|
- 同时启动 10 个 Future,每个 100 条。
|
||||||
|
- 期望:无异常;最终缓存数正确。
|
||||||
|
3. **持续 flush**
|
||||||
|
- 循环 `track + flush` 30 秒。
|
||||||
|
- 期望:无死锁;重试与退避日志正常。
|
||||||
|
|
||||||
|
### C. 错误处理
|
||||||
|
1. **断网**
|
||||||
|
- 断网后 track,恢复网络再 flush。
|
||||||
|
- 期望:队列持久化,恢复后上报成功。
|
||||||
|
2. **后端 5xx**
|
||||||
|
- 指向返回 500 的端点。
|
||||||
|
- 期望:retryCount 增加,事件不丢失。
|
||||||
|
3. **必填标签缺失**
|
||||||
|
- 后端配置必填 tag,track 不带该字段。
|
||||||
|
- 期望:Debug 模式 log warning;Release 模式打 `_sdk_*` 标签。
|
||||||
|
|
||||||
|
### D. 边界与健壮性
|
||||||
|
1. **空参数**
|
||||||
|
- `eventParams=null` 或空对象。
|
||||||
|
- 期望:事件可正常入库。
|
||||||
|
2. **超大 payload**
|
||||||
|
- `eventParams` 约 100KB。
|
||||||
|
- 期望:入库正常,flush 不崩。
|
||||||
|
3. **特殊字符**
|
||||||
|
- `eventType` 含空格/emoji/符号。
|
||||||
|
- 期望:入库正常,后端能接收。
|
||||||
|
4. **生命周期**
|
||||||
|
- 进入后台触发自动 flush。
|
||||||
|
- 期望:日志中出现 flush,缓存减少。
|
||||||
|
5. **Isolate 回退**
|
||||||
|
- 临时在 debug 分支让 Isolate init 抛错,观察回退日志。
|
||||||
|
- 期望:自动降级至 sqflite,功能不受影响。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Priority / 实现优先级
|
||||||
|
|
||||||
|
1. **[High/高]** Stress Test (1000 events) - Already in progress.
|
||||||
|
2. **[High/高]** Error Handling - Network failure and retry behavior.
|
||||||
|
3. **[Medium/中]** Edge Cases - Large payload, special characters.
|
||||||
|
4. **[Medium/中]** SDK Robustness - Exception isolation verification.
|
||||||
|
5. **[Low/低]** Advanced UI - Real-time log display.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps / 下一步
|
||||||
|
1. Finish implementing `_runStressTest` method.
|
||||||
|
2. Add error simulation buttons.
|
||||||
|
3. Add edge case test buttons.
|
||||||
|
4. Add real-time log display widget.
|
||||||
|
5. Add a toggle for `allowInsecureHttp` and `useIsolateStorage` in demo.
|
||||||
|
|
@ -3,21 +3,20 @@ import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
|
|
||||||
|
final AnalyticsConfig _defaultConfig = AnalyticsConfig(
|
||||||
|
systemCode: 'SDK-TEST-FLUTTER',
|
||||||
|
endpointBaseUrl: 'http://192.168.2.7:18828',
|
||||||
|
// clientType: 3, // Auto-detected
|
||||||
|
enableDebug: true,
|
||||||
|
batchSize: 30,
|
||||||
|
flushInterval: 30,
|
||||||
|
allowInsecureHttp: true,
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
await Analytics.init(
|
await Analytics.init(_defaultConfig);
|
||||||
const AnalyticsConfig(
|
|
||||||
systemCode: 'SDK-TEST-FLUTTER',
|
|
||||||
endpointBaseUrl: 'http://192.168.2.7:18828/api/',
|
|
||||||
clientType: 3,
|
|
||||||
enableDebug: true,
|
|
||||||
batchSize: 5,
|
|
||||||
flushInterval: 30,
|
|
||||||
allowInsecureHttp: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Analytics.bindLifecycleObserver();
|
Analytics.bindLifecycleObserver();
|
||||||
|
|
||||||
runApp(const DemoApp());
|
runApp(const DemoApp());
|
||||||
|
|
@ -28,7 +27,10 @@ class DemoApp extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return const MaterialApp(home: DemoPage());
|
return MaterialApp(
|
||||||
|
home: const DemoPage(),
|
||||||
|
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,13 +44,20 @@ class DemoPage extends StatefulWidget {
|
||||||
class _DemoPageState extends State<DemoPage> {
|
class _DemoPageState extends State<DemoPage> {
|
||||||
int _cacheCount = 0;
|
int _cacheCount = 0;
|
||||||
List<RecentEventSummary> _recent = const <RecentEventSummary>[];
|
List<RecentEventSummary> _recent = const <RecentEventSummary>[];
|
||||||
bool _flushing = false;
|
final List<String> _logs = <String>[];
|
||||||
bool _refreshingConfig = false;
|
final Set<String> _running = <String>{};
|
||||||
|
bool _verboseStressLogs = false;
|
||||||
|
bool _sdkDebugEnabled = true;
|
||||||
|
final TextEditingController _mockBaseUrlController = TextEditingController();
|
||||||
|
final ScrollController _logScrollController = ScrollController();
|
||||||
Timer? _pollTimer;
|
Timer? _pollTimer;
|
||||||
|
late AnalyticsConfig _activeConfig;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_activeConfig = _defaultConfig;
|
||||||
|
_sdkDebugEnabled = _activeConfig.enableDebug;
|
||||||
_refreshCount();
|
_refreshCount();
|
||||||
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||||||
_refreshCount();
|
_refreshCount();
|
||||||
|
|
@ -58,9 +67,72 @@ class _DemoPageState extends State<DemoPage> {
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_pollTimer?.cancel();
|
_pollTimer?.cancel();
|
||||||
|
_mockBaseUrlController.dispose();
|
||||||
|
_logScrollController.dispose();
|
||||||
super.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 {
|
Future<void> _refreshCount() async {
|
||||||
final results = await Future.wait(<Future<Object>>[
|
final results = await Future.wait(<Future<Object>>[
|
||||||
Analytics.cachedEventCount(),
|
Analytics.cachedEventCount(),
|
||||||
|
|
@ -77,105 +149,313 @@ class _DemoPageState extends State<DemoPage> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
Future<void> _trackDemoEvent() async {
|
||||||
await Analytics.track(
|
await Analytics.track(
|
||||||
'DEMO_BUTTON_CLICK',
|
'DEMO_BUTTON_CLICK',
|
||||||
eventParams: const <String, dynamic>{
|
eventParams: _requiredParams(
|
||||||
'page': 'demo',
|
page: 'demo',
|
||||||
'Url': 'https://example.com/demo',
|
url: 'https://example.com/demo',
|
||||||
'ButtonId': 'demo_btn_01',
|
buttonId: 'demo_btn_01',
|
||||||
},
|
),
|
||||||
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||||||
);
|
);
|
||||||
await _refreshCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _flushNow() async {
|
Future<void> _flushNow() async {
|
||||||
if (_flushing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_flushing = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await Analytics.flush(force: true);
|
await Analytics.flush(force: true);
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_flushing = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await _refreshCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshConfig() async {
|
Future<void> _refreshConfig() async {
|
||||||
if (_refreshingConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_refreshingConfig = true;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await Analytics.refreshConfig(force: true);
|
await Analytics.refreshConfig(force: true);
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
|
||||||
_refreshingConfig = false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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'},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _runStressConcurrent() async {
|
||||||
Widget build(BuildContext context) {
|
Future<void> trackBatch(int batch, int count) async {
|
||||||
return Scaffold(
|
for (var i = 0; i < count; i += 1) {
|
||||||
appBar: AppBar(title: const Text('YX Tracking Demo')),
|
await Analytics.track(
|
||||||
body: Padding(
|
'STRESS_CONCURRENT',
|
||||||
padding: const EdgeInsets.all(16),
|
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(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||||
'本地缓存事件数:$_cacheCount',
|
const SizedBox(height: 12),
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
...children,
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Wrap(
|
|
||||||
spacing: 12,
|
|
||||||
runSpacing: 12,
|
|
||||||
children: <Widget>[
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _trackDemoEvent,
|
|
||||||
child: const Text('Track Demo Event'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _flushing ? null : _flushNow,
|
|
||||||
child: Text(_flushing ? 'Flushing...' : 'Flush Now'),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: _refreshingConfig ? null : _refreshConfig,
|
|
||||||
child: Text(
|
|
||||||
_refreshingConfig ? 'Refreshing...' : 'Refresh Config',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
const Text(
|
|
||||||
'说明:已对接 SDK-TEST-FLUTTER 系统,'
|
|
||||||
'点击 Track 按钮记录事件,点击 Flush 上报。',
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
);
|
||||||
Text(
|
}
|
||||||
'最近事件(最多 20 条)',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
Widget _buildConfigSummary() {
|
||||||
),
|
return Column(
|
||||||
const SizedBox(height: 8),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Expanded(
|
children: <Widget>[
|
||||||
child: _recent.isEmpty
|
Text('Endpoint: ${_activeConfig.endpointBaseUrl}'),
|
||||||
? const Text('暂无事件')
|
Text('systemCode: ${_activeConfig.systemCode}'),
|
||||||
: ListView.separated(
|
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,
|
itemCount: _recent.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|
@ -188,10 +468,255 @@ class _DemoPageState extends State<DemoPage> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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('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()),
|
||||||
|
]),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import 'dart:core';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:yx_tracking_flutter/src/model/client_type.dart';
|
||||||
|
|
||||||
/// SDK 初始化配置。
|
/// SDK 初始化配置。
|
||||||
class AnalyticsConfig {
|
class AnalyticsConfig {
|
||||||
/// 创建 SDK 配置实例。
|
/// 创建 SDK 配置实例。
|
||||||
const AnalyticsConfig({
|
AnalyticsConfig({
|
||||||
required this.systemCode,
|
required this.systemCode,
|
||||||
required this.endpointBaseUrl,
|
required this.endpointBaseUrl,
|
||||||
required this.clientType,
|
int? clientType,
|
||||||
this.enableDebug = false,
|
this.enableDebug = false,
|
||||||
this.batchSize = 20,
|
this.batchSize = 20,
|
||||||
this.flushInterval = 15,
|
this.flushInterval = 15,
|
||||||
|
|
@ -20,7 +21,25 @@ class AnalyticsConfig {
|
||||||
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();
|
||||||
|
|
||||||
|
/// 自动探测客户端类型。
|
||||||
|
static int _detectClientType() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return ClientType.h5.value;
|
||||||
|
}
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return ClientType.android.value;
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return ClientType.ios.value;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
return ClientType.pc.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 系统编码(system_code)。
|
/// 系统编码(system_code)。
|
||||||
final String systemCode;
|
final String systemCode;
|
||||||
|
|
@ -86,6 +105,13 @@ class AnalyticsConfig {
|
||||||
'endpointBaseUrl 不是合法的 URL',
|
'endpointBaseUrl 不是合法的 URL',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (uri.path.isNotEmpty && uri.path != '/') {
|
||||||
|
throw ArgumentError.value(
|
||||||
|
endpointBaseUrl,
|
||||||
|
'endpointBaseUrl',
|
||||||
|
'endpointBaseUrl 仅允许配置基础 host(不包含路径)',
|
||||||
|
);
|
||||||
|
}
|
||||||
final scheme = uri.scheme.toLowerCase();
|
final scheme = uri.scheme.toLowerCase();
|
||||||
if (scheme != 'https') {
|
if (scheme != 'https') {
|
||||||
if (scheme == 'http' && allowInsecureHttp) {
|
if (scheme == 'http' && allowInsecureHttp) {
|
||||||
|
|
@ -166,23 +192,34 @@ class AnalyticsConfig {
|
||||||
|
|
||||||
/// `/AddEventListLog` 的完整地址。
|
/// `/AddEventListLog` 的完整地址。
|
||||||
Uri get addEventListLogUri =>
|
Uri get addEventListLogUri =>
|
||||||
_appendPath('ExternalEventlogs/AddEventListLog');
|
_appendPath('api/ExternalEventlogs/AddEventListLog');
|
||||||
|
|
||||||
/// `/AddEventLog` 的完整地址(可选降级)。
|
/// `/AddEventLog` 的完整地址(可选降级)。
|
||||||
Uri get addEventLogUri => _appendPath('ExternalEventlogs/AddEventLog');
|
Uri get addEventLogUri => _appendPath('api/ExternalEventlogs/AddEventLog');
|
||||||
|
|
||||||
/// `/GetSystemAllDimInfo` 的完整地址(Phase 2+)。
|
/// `/GetSystemAllDimInfo` 的完整地址(Phase 2+)。
|
||||||
Uri get getSystemAllDimInfoUri =>
|
Uri get getSystemAllDimInfoUri =>
|
||||||
_appendPath('ExternalEventlogs/GetSystemAllDimInfo');
|
_appendPath('api/ExternalEventlogs/GetSystemAllDimInfo');
|
||||||
|
|
||||||
Uri _appendPath(String leaf) {
|
Uri _appendPath(String leaf) {
|
||||||
final base = Uri.parse(endpointBaseUrl);
|
final base = Uri.parse(endpointBaseUrl);
|
||||||
final normalizedBasePath = base.path.endsWith('/')
|
final normalizedBasePath = base.path.endsWith('/')
|
||||||
? base.path.substring(0, base.path.length - 1)
|
? base.path.substring(0, base.path.length - 1)
|
||||||
: base.path;
|
: base.path;
|
||||||
final nextPath =
|
final normalizedLeaf = _normalizeLeaf(normalizedBasePath, leaf);
|
||||||
normalizedBasePath.isEmpty ? '/$leaf' : '$normalizedBasePath/$leaf';
|
final nextPath = normalizedBasePath.isEmpty
|
||||||
|
? '/$normalizedLeaf'
|
||||||
|
: '$normalizedBasePath/$normalizedLeaf';
|
||||||
|
|
||||||
return base.replace(path: nextPath);
|
return base.replace(path: nextPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _normalizeLeaf(String basePath, String leaf) {
|
||||||
|
const externalSegment = 'ExternalEventlogs/';
|
||||||
|
if (basePath.endsWith('/ExternalEventlogs') &&
|
||||||
|
leaf.startsWith(externalSegment)) {
|
||||||
|
return leaf.substring(externalSegment.length);
|
||||||
|
}
|
||||||
|
return leaf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ class ConfigManager {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'配置拉取并缓存成功: events=$eventCount, tags=$tagCount',
|
'配置拉取并缓存成功: events=$eventCount, tags=$tagCount',
|
||||||
);
|
);
|
||||||
|
_logConfigDetails(info);
|
||||||
} on DioException catch (e, st) {
|
} on DioException catch (e, st) {
|
||||||
Logger.error('配置拉取失败(DioException)', e, st);
|
Logger.error('配置拉取失败(DioException)', e, st);
|
||||||
} on Object catch (e, st) {
|
} on Object catch (e, st) {
|
||||||
|
|
@ -135,4 +136,33 @@ class ConfigManager {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _logConfigDetails(SystemDimInfo info) {
|
||||||
|
final events = info.eventDefinitions
|
||||||
|
.map(
|
||||||
|
(event) => event.eventName == null || event.eventName!.trim().isEmpty
|
||||||
|
? event.eventCode
|
||||||
|
: '${event.eventCode}(${event.eventName})',
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
final tags = info.tagDefinitions
|
||||||
|
.map(
|
||||||
|
(tag) =>
|
||||||
|
'${tag.tagName}:${tag.tagType}${tag.isRequired ? '*' : ''}',
|
||||||
|
)
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
'事件定义: ${events.isEmpty ? 'none' : events.join(', ')}',
|
||||||
|
);
|
||||||
|
Logger.info(
|
||||||
|
'标签定义: ${tags.isEmpty ? 'none' : tags.join(', ')}',
|
||||||
|
);
|
||||||
|
final strategy = info.sdkStrategy;
|
||||||
|
if (strategy != null) {
|
||||||
|
Logger.info(
|
||||||
|
'策略: enabled=${strategy.enabled}, defaultSampleRate=${strategy.defaultSampleRate}, eventSettings=${strategy.eventSettings.length}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/// 客户端类型枚举。
|
||||||
|
enum ClientType {
|
||||||
|
/// 安卓 (1)
|
||||||
|
android(1),
|
||||||
|
|
||||||
|
/// iOS (2)
|
||||||
|
ios(2),
|
||||||
|
|
||||||
|
/// PC (3)
|
||||||
|
pc(3),
|
||||||
|
|
||||||
|
/// H5 (4)
|
||||||
|
h5(4);
|
||||||
|
|
||||||
|
const ClientType(this.value);
|
||||||
|
|
||||||
|
/// 对应的整数值。
|
||||||
|
final int value;
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,9 @@ class HttpClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _normalizeBaseUrl(String baseUrl) {
|
static String _normalizeBaseUrl(String baseUrl) {
|
||||||
|
if (baseUrl.endsWith('/') && baseUrl.length > 1) {
|
||||||
|
return baseUrl.substring(0, baseUrl.length - 1);
|
||||||
|
}
|
||||||
return baseUrl;
|
return baseUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ AnalyticsConfig _base({
|
||||||
}) {
|
}) {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: systemCode ?? 'SYS',
|
systemCode: systemCode ?? 'SYS',
|
||||||
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com/api',
|
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com',
|
||||||
clientType: clientType ?? 3,
|
clientType: clientType ?? 3,
|
||||||
batchSize: batchSize ?? 20,
|
batchSize: batchSize ?? 20,
|
||||||
flushInterval: flushInterval ?? 15,
|
flushInterval: flushInterval ?? 15,
|
||||||
|
|
@ -49,6 +49,13 @@ void main() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('包含路径的 baseUrl 会抛错', () {
|
||||||
|
expect(
|
||||||
|
() => _base(endpointBaseUrl: 'https://example.com/api').validate(),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('非 https 会抛错', () {
|
test('非 https 会抛错', () {
|
||||||
expect(
|
expect(
|
||||||
() => _base(endpointBaseUrl: 'http://example.com').validate(),
|
() => _base(endpointBaseUrl: 'http://example.com').validate(),
|
||||||
|
|
@ -123,18 +130,36 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('AnalyticsConfig.uri 组装', () {
|
group('AnalyticsConfig.uri 组装', () {
|
||||||
test('basePath 为空时会补 /leaf', () {
|
test('基础 host 会拼接 api/ExternalEventlogs 路径', () {
|
||||||
final config = _base(endpointBaseUrl: 'https://example.com');
|
final config = _base(endpointBaseUrl: 'https://example.com');
|
||||||
expect(config.addEventListLogUri.path, '/AddEventListLog');
|
expect(
|
||||||
expect(config.addEventLogUri.path, '/AddEventLog');
|
config.addEventListLogUri.path,
|
||||||
expect(config.getSystemAllDimInfoUri.path, '/GetSystemAllDimInfo');
|
'/api/ExternalEventlogs/AddEventListLog',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
config.addEventLogUri.path,
|
||||||
|
'/api/ExternalEventlogs/AddEventLog',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
config.getSystemAllDimInfoUri.path,
|
||||||
|
'/api/ExternalEventlogs/GetSystemAllDimInfo',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('basePath 有值且带斜杠时会规范化', () {
|
test('末尾带斜杠的 host 也可正常拼接', () {
|
||||||
final config = _base(endpointBaseUrl: 'https://example.com/api/');
|
final config = _base(endpointBaseUrl: 'https://example.com/');
|
||||||
expect(config.addEventListLogUri.path, '/api/AddEventListLog');
|
expect(
|
||||||
expect(config.addEventLogUri.path, '/api/AddEventLog');
|
config.addEventListLogUri.path,
|
||||||
expect(config.getSystemAllDimInfoUri.path, '/api/GetSystemAllDimInfo');
|
'/api/ExternalEventlogs/AddEventListLog',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
config.addEventLogUri.path,
|
||||||
|
'/api/ExternalEventlogs/AddEventLog',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
config.getSystemAllDimInfoUri.path,
|
||||||
|
'/api/ExternalEventlogs/GetSystemAllDimInfo',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -290,7 +290,7 @@ AnalyticsConfig _testConfig({
|
||||||
}) {
|
}) {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: enableDebug,
|
enableDebug: enableDebug,
|
||||||
batchSize: batchSize,
|
batchSize: batchSize,
|
||||||
|
|
@ -751,9 +751,9 @@ void main() {
|
||||||
final storage = MemoryEventStorage();
|
final storage = MemoryEventStorage();
|
||||||
late FakeApiClient apiClient;
|
late FakeApiClient apiClient;
|
||||||
late TestConfigManager configManager;
|
late TestConfigManager configManager;
|
||||||
const config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 10,
|
batchSize: 10,
|
||||||
|
|
@ -879,9 +879,9 @@ void main() {
|
||||||
schedulerFactory: _noopScheduler,
|
schedulerFactory: _noopScheduler,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
|
|
@ -1206,9 +1206,9 @@ void main() {
|
||||||
schedulerFactory: _noopScheduler,
|
schedulerFactory: _noopScheduler,
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
metricsReportInterval: Duration(milliseconds: 10),
|
metricsReportInterval: Duration(milliseconds: 10),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:yx_tracking_flutter/src/network/api_client.dart';
|
||||||
import 'package:yx_tracking_flutter/src/network/http_client.dart';
|
import 'package:yx_tracking_flutter/src/network/http_client.dart';
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return const AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'SYS',
|
systemCode: 'SYS',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,9 @@ class ThrowingHttpClient extends HttpClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return const AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,9 @@ class _NoopScheduler extends Scheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return const AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
metricsReportInterval: Duration(days: 1),
|
metricsReportInterval: Duration(days: 1),
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ void main() {
|
||||||
test('headers 为空时不会创建 Options', () async {
|
test('headers 为空时不会创建 Options', () async {
|
||||||
final adapter = _FakeAdapter();
|
final adapter = _FakeAdapter();
|
||||||
final client = HttpClient(
|
final client = HttpClient(
|
||||||
_config('https://example.com/api'),
|
_config('https://example.com'),
|
||||||
httpClientAdapter: adapter,
|
httpClientAdapter: adapter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ void main() {
|
||||||
test('会把 headers 透传到请求', () async {
|
test('会把 headers 透传到请求', () async {
|
||||||
final adapter = _FakeAdapter();
|
final adapter = _FakeAdapter();
|
||||||
final client = HttpClient(
|
final client = HttpClient(
|
||||||
_config('https://example.com/api'),
|
_config('https://example.com'),
|
||||||
httpClientAdapter: adapter,
|
httpClientAdapter: adapter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ const DeviceInfo _device = DeviceInfo(
|
||||||
);
|
);
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return const AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||||||
void main() {
|
void main() {
|
||||||
group('AnalyticsConfig.validate', () {
|
group('AnalyticsConfig.validate', () {
|
||||||
test('https 配置通过校验', () {
|
test('https 配置通过校验', () {
|
||||||
const config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'https://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -15,9 +15,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('非 https 配置会抛错', () {
|
test('非 https 配置会抛错', () {
|
||||||
const config = AnalyticsConfig(
|
final config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'http://example.com/api/ExternalEventlogs',
|
endpointBaseUrl: 'http://example.com',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue