Compare commits
No commits in common. "ee9e6739b5abf17f5f980c405469de67736d4f4e" and "29c60bbc4848e4fefc239978f583c42210831218" have entirely different histories.
ee9e6739b5
...
29c60bbc48
|
|
@ -47,9 +47,7 @@
|
||||||
"screenResolution": "string"
|
"screenResolution": "string"
|
||||||
},
|
},
|
||||||
"eventParams": {
|
"eventParams": {
|
||||||
"Page": "string",
|
// 事件上下文
|
||||||
"Url": "string",
|
|
||||||
"ButtonId": "string"
|
|
||||||
},
|
},
|
||||||
"customTags": {
|
"customTags": {
|
||||||
// 业务维度
|
// 业务维度
|
||||||
|
|
@ -92,7 +90,7 @@ class Analytics {
|
||||||
```dart
|
```dart
|
||||||
class AnalyticsConfig {
|
class AnalyticsConfig {
|
||||||
final String systemCode;
|
final String systemCode;
|
||||||
final String endpointBaseUrl; // https://host(仅基础 host)
|
final String endpointBaseUrl; // https://host/api/ExternalEventlogs
|
||||||
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 基础地址** | `http://192.168.2.7:18828/api/ExternalEventlogs` |
|
||||||
| **系统标识** | `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',
|
- endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
+ systemCode: 'SDK-TEST-FLUTTER',
|
+ systemCode: 'SDK-TEST-FLUTTER',
|
||||||
+ endpointBaseUrl: 'http://192.168.2.7:18828',
|
+ endpointBaseUrl: 'http://192.168.2.7:18828/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 5,
|
batchSize: 5,
|
||||||
|
|
@ -154,11 +154,7 @@ Content-Type: application/json
|
||||||
"model": "MacBook Pro",
|
"model": "MacBook Pro",
|
||||||
"screenResolution": "1920x1080"
|
"screenResolution": "1920x1080"
|
||||||
},
|
},
|
||||||
"eventParams": {
|
"eventParams": {"page": "demo"},
|
||||||
"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',
|
endpointBaseUrl: 'https://your-host/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
),
|
),
|
||||||
|
|
@ -47,11 +47,7 @@ Future<void> bootstrapAnalytics() async {
|
||||||
```dart
|
```dart
|
||||||
await Analytics.track(
|
await Analytics.track(
|
||||||
'PAGE_VIEW',
|
'PAGE_VIEW',
|
||||||
eventParams: const <String, dynamic>{
|
eventParams: const <String, dynamic>{'page': 'home'},
|
||||||
'Page': 'home',
|
|
||||||
'Url': 'https://example.com/home',
|
|
||||||
'ButtonId': 'page_view',
|
|
||||||
},
|
|
||||||
customTags: const <String, dynamic>{'tenantId': 't1'},
|
customTags: const <String, dynamic>{'tenantId': 't1'},
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
@ -65,7 +61,6 @@ 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`)
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
# 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,20 +3,21 @@ 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(_defaultConfig);
|
await Analytics.init(
|
||||||
|
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());
|
||||||
|
|
@ -27,10 +28,7 @@ class DemoApp extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return const MaterialApp(home: DemoPage());
|
||||||
home: const DemoPage(),
|
|
||||||
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,20 +42,13 @@ 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>[];
|
||||||
final List<String> _logs = <String>[];
|
bool _flushing = false;
|
||||||
final Set<String> _running = <String>{};
|
bool _refreshingConfig = false;
|
||||||
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();
|
||||||
|
|
@ -67,72 +58,9 @@ 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(),
|
||||||
|
|
@ -149,601 +77,121 @@ 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: _requiredParams(
|
eventParams: const <String, dynamic>{
|
||||||
page: 'demo',
|
'page': 'demo',
|
||||||
url: 'https://example.com/demo',
|
'Url': 'https://example.com/demo',
|
||||||
buttonId: 'demo_btn_01',
|
'ButtonId': 'demo_btn_01',
|
||||||
),
|
},
|
||||||
customTags: TagTemplates.merge([
|
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||||||
TagTemplates.businessContext(
|
|
||||||
tenantId: 't1',
|
|
||||||
appVersion: '1.0.0',
|
|
||||||
channel: 'internal_test',
|
|
||||||
),
|
|
||||||
TagTemplates.forScreen(screenName: 'DemoPage', featureModule: 'demo'),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
await _refreshCount();
|
||||||
|
|
||||||
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 {
|
Future<void> _flushNow() async {
|
||||||
await Analytics.flush(force: true);
|
if (_flushing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_flushing = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await Analytics.flush(force: true);
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_flushing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await _refreshCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshConfig() async {
|
Future<void> _refreshConfig() async {
|
||||||
await Analytics.refreshConfig(force: true);
|
if (_refreshingConfig) {
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
final original = _activeConfig;
|
setState(() {
|
||||||
final tempConfig = _buildConfig(
|
_refreshingConfig = true;
|
||||||
endpointBaseUrl: mock,
|
});
|
||||||
allowInsecureHttp: mock.startsWith('http://'),
|
try {
|
||||||
);
|
await Analytics.refreshConfig(force: true);
|
||||||
await _reinitialize(tempConfig, reason: '5xx 模拟');
|
} finally {
|
||||||
await Analytics.track(
|
if (mounted) {
|
||||||
'EVENT_5XX_TEST',
|
setState(() {
|
||||||
eventParams: _requiredParams(
|
_refreshingConfig = false;
|
||||||
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('YX Tracking Demo')),
|
appBar: AppBar(title: const Text('YX Tracking Demo')),
|
||||||
body: ListView(
|
body: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: <Widget>[
|
child: Column(
|
||||||
Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'本地缓存事件数:$_cacheCount',
|
children: <Widget>[
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
Text(
|
||||||
),
|
'本地缓存事件数:$_cacheCount',
|
||||||
const SizedBox(height: 8),
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
_buildSection('配置摘要', <Widget>[_buildConfigSummary()]),
|
),
|
||||||
_buildSection('基础操作', <Widget>[
|
const SizedBox(height: 16),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
runSpacing: 12,
|
runSpacing: 12,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isRunning('track_demo')
|
onPressed: _trackDemoEvent,
|
||||||
? null
|
|
||||||
: () => _runAction(
|
|
||||||
'track_demo',
|
|
||||||
'Track Demo Event',
|
|
||||||
_trackDemoEvent,
|
|
||||||
),
|
|
||||||
child: const Text('Track Demo Event'),
|
child: const Text('Track Demo Event'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: _isRunning('set_user')
|
onPressed: _flushing ? null : _flushNow,
|
||||||
? null
|
child: Text(_flushing ? 'Flushing...' : 'Flush Now'),
|
||||||
: () => _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(
|
OutlinedButton(
|
||||||
onPressed: _isRunning('refresh_config')
|
onPressed: _refreshingConfig ? null : _refreshConfig,
|
||||||
? null
|
|
||||||
: () => _runAction(
|
|
||||||
'refresh_config',
|
|
||||||
'Refresh Config',
|
|
||||||
_refreshConfig,
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
_isRunning('refresh_config')
|
_refreshingConfig ? 'Refreshing...' : '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 SizedBox(height: 12),
|
||||||
const Text('日志输出'),
|
const Text(
|
||||||
|
'说明:已对接 SDK-TEST-FLUTTER 系统,'
|
||||||
|
'点击 Track 按钮记录事件,点击 Flush 上报。',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
'最近事件(最多 20 条)',
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SizedBox(height: 160, child: _buildLogList()),
|
Expanded(
|
||||||
]),
|
child: _recent.isEmpty
|
||||||
],
|
? const Text('暂无事件')
|
||||||
|
: 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}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,10 @@
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import device_info_plus
|
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,22 +49,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.8"
|
||||||
device_info_plus:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus
|
|
||||||
sha256: "4df8babf73058181227e18b08e6ea3520cf5fc5d796888d33b7cb0f33f984b7c"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "12.3.0"
|
|
||||||
device_info_plus_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: device_info_plus_platform_interface
|
|
||||||
sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.3"
|
|
||||||
dio:
|
dio:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -97,14 +81,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.5"
|
||||||
file:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: file
|
|
||||||
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.1"
|
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -123,11 +99,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
flutter_web_plugins:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -405,22 +376,6 @@ packages:
|
||||||
url: "https://pub.flutter-io.cn"
|
url: "https://pub.flutter-io.cn"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
win32:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: win32
|
|
||||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "5.15.0"
|
|
||||||
win32_registry:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: win32_registry
|
|
||||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
|
||||||
url: "https://pub.flutter-io.cn"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import 'package:flutter/foundation.dart';
|
import 'dart:core';
|
||||||
import 'package:yx_tracking_flutter/src/model/client_type.dart';
|
|
||||||
|
|
||||||
/// SDK 初始化配置。
|
/// SDK 初始化配置。
|
||||||
class AnalyticsConfig {
|
class AnalyticsConfig {
|
||||||
/// 创建 SDK 配置实例。
|
/// 创建 SDK 配置实例。
|
||||||
AnalyticsConfig({
|
const AnalyticsConfig({
|
||||||
required this.systemCode,
|
required this.systemCode,
|
||||||
required this.endpointBaseUrl,
|
required this.endpointBaseUrl,
|
||||||
int? clientType,
|
required this.clientType,
|
||||||
this.enableDebug = false,
|
this.enableDebug = false,
|
||||||
this.batchSize = 20,
|
this.batchSize = 20,
|
||||||
this.flushInterval = 15,
|
this.flushInterval = 15,
|
||||||
|
|
@ -21,25 +20,7 @@ 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;
|
||||||
|
|
@ -105,13 +86,6 @@ 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) {
|
||||||
|
|
@ -192,34 +166,23 @@ class AnalyticsConfig {
|
||||||
|
|
||||||
/// `/AddEventListLog` 的完整地址。
|
/// `/AddEventListLog` 的完整地址。
|
||||||
Uri get addEventListLogUri =>
|
Uri get addEventListLogUri =>
|
||||||
_appendPath('api/ExternalEventlogs/AddEventListLog');
|
_appendPath('ExternalEventlogs/AddEventListLog');
|
||||||
|
|
||||||
/// `/AddEventLog` 的完整地址(可选降级)。
|
/// `/AddEventLog` 的完整地址(可选降级)。
|
||||||
Uri get addEventLogUri => _appendPath('api/ExternalEventlogs/AddEventLog');
|
Uri get addEventLogUri => _appendPath('ExternalEventlogs/AddEventLog');
|
||||||
|
|
||||||
/// `/GetSystemAllDimInfo` 的完整地址(Phase 2+)。
|
/// `/GetSystemAllDimInfo` 的完整地址(Phase 2+)。
|
||||||
Uri get getSystemAllDimInfoUri =>
|
Uri get getSystemAllDimInfoUri =>
|
||||||
_appendPath('api/ExternalEventlogs/GetSystemAllDimInfo');
|
_appendPath('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 normalizedLeaf = _normalizeLeaf(normalizedBasePath, leaf);
|
final nextPath =
|
||||||
final nextPath = normalizedBasePath.isEmpty
|
normalizedBasePath.isEmpty ? '/$leaf' : '$normalizedBasePath/$leaf';
|
||||||
? '/$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,7 +77,6 @@ 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) {
|
||||||
|
|
@ -136,33 +135,4 @@ 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}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/// 客户端类型枚举。
|
|
||||||
enum ClientType {
|
|
||||||
/// 安卓 (1)
|
|
||||||
android(1),
|
|
||||||
|
|
||||||
/// iOS (2)
|
|
||||||
ios(2),
|
|
||||||
|
|
||||||
/// PC (3)
|
|
||||||
pc(3),
|
|
||||||
|
|
||||||
/// H5 (4)
|
|
||||||
h5(4);
|
|
||||||
|
|
||||||
const ClientType(this.value);
|
|
||||||
|
|
||||||
/// 对应的整数值。
|
|
||||||
final int value;
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ class DeviceInfo {
|
||||||
required this.screenResolution,
|
required this.screenResolution,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 操作系统名称或版本(例如 "Android 10" 或 "Version 14.4")。
|
/// 操作系统名称或版本。
|
||||||
final String os;
|
final String os;
|
||||||
|
|
||||||
/// 设备型号。
|
/// 设备型号。
|
||||||
|
|
|
||||||
|
|
@ -86,9 +86,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:device_info_plus/device_info_plus.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:yx_tracking_flutter/src/model/device_info.dart';
|
import 'package:yx_tracking_flutter/src/model/device_info.dart';
|
||||||
|
|
@ -19,53 +17,10 @@ class DeviceUtil {
|
||||||
static Future<DeviceInfo> collectDeviceInfo() async {
|
static Future<DeviceInfo> collectDeviceInfo() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
var os = 'unknown';
|
final os = Platform.operatingSystem;
|
||||||
var model = 'unknown';
|
final model = Platform.operatingSystemVersion;
|
||||||
final screenResolution = _screenResolution();
|
final screenResolution = _screenResolution();
|
||||||
|
|
||||||
try {
|
|
||||||
final deviceInfo = DeviceInfoPlugin();
|
|
||||||
if (kIsWeb) {
|
|
||||||
final webInfo = await deviceInfo.webBrowserInfo;
|
|
||||||
os = '${webInfo.browserName.name} ${webInfo.appVersion}';
|
|
||||||
model = webInfo.userAgent ?? 'unknown';
|
|
||||||
} else {
|
|
||||||
if (Platform.isAndroid) {
|
|
||||||
final androidInfo = await deviceInfo.androidInfo;
|
|
||||||
// 例如: Android 12
|
|
||||||
os = 'Android ${androidInfo.version.release}';
|
|
||||||
// 例如: Google Pixel 6
|
|
||||||
model = '${androidInfo.brand} ${androidInfo.model}';
|
|
||||||
} else if (Platform.isIOS) {
|
|
||||||
final iosInfo = await deviceInfo.iosInfo;
|
|
||||||
// 例如: iOS 15.4
|
|
||||||
os = '${iosInfo.systemName} ${iosInfo.systemVersion}';
|
|
||||||
// 例如: iPhone14,2
|
|
||||||
model = iosInfo.utsname.machine;
|
|
||||||
} else if (Platform.isMacOS) {
|
|
||||||
final macInfo = await deviceInfo.macOsInfo;
|
|
||||||
os = 'macOS ${macInfo.majorVersion}.${macInfo.minorVersion}';
|
|
||||||
model = macInfo.model;
|
|
||||||
} else if (Platform.isWindows) {
|
|
||||||
final windowsInfo = await deviceInfo.windowsInfo;
|
|
||||||
os =
|
|
||||||
'Windows ${windowsInfo.majorVersion}.${windowsInfo.minorVersion}';
|
|
||||||
model = 'PC';
|
|
||||||
} else if (Platform.isLinux) {
|
|
||||||
final linuxInfo = await deviceInfo.linuxInfo;
|
|
||||||
os = 'Linux ${linuxInfo.versionId}';
|
|
||||||
model = linuxInfo.name;
|
|
||||||
} else {
|
|
||||||
os = Platform.operatingSystemVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Fallback if plugin fails
|
|
||||||
if (!kIsWeb) {
|
|
||||||
os = Platform.operatingSystemVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
os: os,
|
os: os,
|
||||||
model: model,
|
model: model,
|
||||||
|
|
@ -74,7 +29,6 @@ class DeviceUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
|
|
||||||
/// 测试钩子:通过注入 view 列表计算分辨率。
|
/// 测试钩子:通过注入 view 列表计算分辨率。
|
||||||
static String screenResolutionForTesting({
|
static String screenResolutionForTesting({
|
||||||
Iterable<FlutterView> Function()? viewsProvider,
|
Iterable<FlutterView> Function()? viewsProvider,
|
||||||
|
|
@ -83,7 +37,6 @@ class DeviceUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
|
|
||||||
/// 测试钩子:通过 size 计算分辨率字符串。
|
/// 测试钩子:通过 size 计算分辨率字符串。
|
||||||
static String resolutionFromSizeForTesting(Size size) {
|
static String resolutionFromSizeForTesting(Size size) {
|
||||||
return _resolutionFromSize(size);
|
return _resolutionFromSize(size);
|
||||||
|
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
/// 自定义标签模板工具类。
|
|
||||||
///
|
|
||||||
/// 提供符合行业标准(Firebase GA4、Amplitude、Mixpanel)的自定义标签模板,
|
|
||||||
/// 使用 snake_case 命名规范,便于与数据仓库(BigQuery、Snowflake)集成。
|
|
||||||
///
|
|
||||||
/// ## 设计原则 / Design Principle
|
|
||||||
///
|
|
||||||
/// `TagTemplates` 是 `Event` 的**补充**,不重复 `Event` 已有的字段:
|
|
||||||
///
|
|
||||||
/// | Event 已有字段 | 说明 |
|
|
||||||
/// |---|---|
|
|
||||||
/// | `systemCode` | 系统编码(配置提供) |
|
|
||||||
/// | `eventType` | 事件类型(调用时传入) |
|
|
||||||
/// | `userInfo` | 用户信息(`Analytics.setUser` 设置) |
|
|
||||||
/// | `clientType` | 客户端类型(自动检测:iOS/Android/Web) |
|
|
||||||
/// | `deviceInfo` | 设备信息(自动采集:OS/Model/屏幕) |
|
|
||||||
/// | `timestamp` | 时间戳(自动生成) |
|
|
||||||
///
|
|
||||||
/// `TagTemplates` 提供 `Event` **未覆盖**的业务维度标签。
|
|
||||||
class TagTemplates {
|
|
||||||
/// 私有构造,避免外部实例化。
|
|
||||||
TagTemplates._();
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 业务上下文标签 / Business Context Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建业务上下文标签。
|
|
||||||
///
|
|
||||||
/// > **注意**: 不包含 `platform`,因为 `Event.clientType` 已自动检测平台。
|
|
||||||
///
|
|
||||||
/// - [tenantId]: 租户 ID(多租户系统必填)。
|
|
||||||
/// - [appVersion]: 应用版本号,例如 "1.2.3"。
|
|
||||||
/// - [channel]: 分发渠道,例如 "appstore", "huawei", "xiaomi"。
|
|
||||||
/// - [abTestGroup]: A/B 测试组,例如 "control", "variant_a"。
|
|
||||||
/// - [environment]: 环境标识,例如 "dev", "staging", "prod"。
|
|
||||||
static Map<String, dynamic> businessContext({
|
|
||||||
required String tenantId,
|
|
||||||
String? appVersion,
|
|
||||||
String? channel,
|
|
||||||
String? abTestGroup,
|
|
||||||
String? environment,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'tenant_id': tenantId,
|
|
||||||
if (appVersion != null) 'app_version': appVersion,
|
|
||||||
if (channel != null) 'channel': channel,
|
|
||||||
if (abTestGroup != null) 'ab_test_group': abTestGroup,
|
|
||||||
if (environment != null) 'environment': environment,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 用户分群标签 / User Segmentation Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建用户分群标签。
|
|
||||||
///
|
|
||||||
/// - [userType]: 用户类型,例如 "free", "premium", "enterprise"。
|
|
||||||
/// - [subscriptionTier]: 订阅级别,例如 "basic", "pro", "enterprise"。
|
|
||||||
/// - [isFirstTimeUser]: 是否为首次使用用户。
|
|
||||||
/// - [daysFromSignup]: 注册天数,用于生命周期分析。
|
|
||||||
/// - [cohort]: 用户群组标识,例如 "2024-01", "beta_testers"。
|
|
||||||
/// - [acquisitionSource]: 获客来源,例如 "organic", "paid", "referral"。
|
|
||||||
/// - [lifetimeValueTier]: 用户价值层级,例如 "high", "medium", "low"。
|
|
||||||
static Map<String, dynamic> userSegmentation({
|
|
||||||
String? userType,
|
|
||||||
String? subscriptionTier,
|
|
||||||
bool? isFirstTimeUser,
|
|
||||||
int? daysFromSignup,
|
|
||||||
String? cohort,
|
|
||||||
String? acquisitionSource,
|
|
||||||
String? lifetimeValueTier,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (userType != null) 'user_type': userType,
|
|
||||||
if (subscriptionTier != null) 'subscription_tier': subscriptionTier,
|
|
||||||
if (isFirstTimeUser != null) 'is_first_time_user': isFirstTimeUser,
|
|
||||||
if (daysFromSignup != null) 'days_from_signup': daysFromSignup,
|
|
||||||
if (cohort != null) 'cohort': cohort,
|
|
||||||
if (acquisitionSource != null) 'acquisition_source': acquisitionSource,
|
|
||||||
if (lifetimeValueTier != null) 'lifetime_value_tier': lifetimeValueTier,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 功能追踪标签 / Feature Tracking Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建页面/功能追踪标签。
|
|
||||||
///
|
|
||||||
/// - [screenName]: 当前页面名称。
|
|
||||||
/// - [featureModule]: 功能模块,例如 "profile", "checkout", "search"。
|
|
||||||
/// - [entrySource]: 入口来源,例如 "deeplink", "push", "organic"。
|
|
||||||
/// - [previousScreen]: 上一个页面名称(用于漏斗分析)。
|
|
||||||
/// - [sessionDepth]: 当前会话中的页面深度。
|
|
||||||
static Map<String, dynamic> forScreen({
|
|
||||||
required String screenName,
|
|
||||||
String? featureModule,
|
|
||||||
String? entrySource,
|
|
||||||
String? previousScreen,
|
|
||||||
int? sessionDepth,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'screen_name': screenName,
|
|
||||||
if (featureModule != null) 'feature_module': featureModule,
|
|
||||||
if (entrySource != null) 'entry_source': entrySource,
|
|
||||||
if (previousScreen != null) 'previous_screen': previousScreen,
|
|
||||||
if (sessionDepth != null) 'session_depth': sessionDepth,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 技术上下文标签 / Technical Context Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建技术上下文标签。
|
|
||||||
///
|
|
||||||
/// - [networkType]: 网络类型,例如 "wifi", "4g", "5g"。
|
|
||||||
/// - [isOffline]: 是否离线模式。
|
|
||||||
/// - [buildMode]: 构建模式,例如 "debug", "release", "profile"。
|
|
||||||
/// - [appInstallSource]: 安装来源,例如 "play_store", "app_store"。
|
|
||||||
/// - [locale]: 语言区域,例如 "zh_CN", "en_US"。
|
|
||||||
/// - [timezone]: 时区,例如 "Asia/Shanghai", "America/New_York"。
|
|
||||||
static Map<String, dynamic> technicalContext({
|
|
||||||
String? networkType,
|
|
||||||
bool? isOffline,
|
|
||||||
String? buildMode,
|
|
||||||
String? appInstallSource,
|
|
||||||
String? locale,
|
|
||||||
String? timezone,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (networkType != null) 'network_type': networkType,
|
|
||||||
if (isOffline != null) 'is_offline': isOffline,
|
|
||||||
if (buildMode != null) 'build_mode': buildMode,
|
|
||||||
if (appInstallSource != null) 'app_install_source': appInstallSource,
|
|
||||||
if (locale != null) 'locale': locale,
|
|
||||||
if (timezone != null) 'timezone': timezone,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 营销归因标签 / Campaign Attribution Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建营销归因标签(UTM 参数)。
|
|
||||||
///
|
|
||||||
/// - [campaignId]: 活动 ID。
|
|
||||||
/// - [campaignName]: 活动名称。
|
|
||||||
/// - [campaignSource]: 流量来源,例如 "google", "facebook", "tiktok"。
|
|
||||||
/// - [campaignMedium]: 媒介类型,例如 "cpc", "email", "social"。
|
|
||||||
/// - [campaignTerm]: 搜索关键词(付费搜索)。
|
|
||||||
/// - [campaignContent]: 广告内容/创意标识。
|
|
||||||
static Map<String, dynamic> campaign({
|
|
||||||
String? campaignId,
|
|
||||||
String? campaignName,
|
|
||||||
String? campaignSource,
|
|
||||||
String? campaignMedium,
|
|
||||||
String? campaignTerm,
|
|
||||||
String? campaignContent,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (campaignId != null) 'campaign_id': campaignId,
|
|
||||||
if (campaignName != null) 'campaign_name': campaignName,
|
|
||||||
if (campaignSource != null) 'campaign_source': campaignSource,
|
|
||||||
if (campaignMedium != null) 'campaign_medium': campaignMedium,
|
|
||||||
if (campaignTerm != null) 'campaign_term': campaignTerm,
|
|
||||||
if (campaignContent != null) 'campaign_content': campaignContent,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 电商标签 / E-commerce Tags (Firebase GA4 Compatible)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建电商标签(兼容 Firebase GA4 标准)。
|
|
||||||
///
|
|
||||||
/// - [currency]: 货币代码,例如 "CNY", "USD"。
|
|
||||||
/// - [value]: 交易金额。
|
|
||||||
/// - [transactionId]: 交易 ID。
|
|
||||||
/// - [paymentMethod]: 支付方式,例如 "alipay", "wechat_pay", "credit_card"。
|
|
||||||
/// - [coupon]: 优惠券代码。
|
|
||||||
/// - [shipping]: 运费。
|
|
||||||
/// - [tax]: 税费。
|
|
||||||
static Map<String, dynamic> ecommerce({
|
|
||||||
String? currency,
|
|
||||||
double? value,
|
|
||||||
String? transactionId,
|
|
||||||
String? paymentMethod,
|
|
||||||
String? coupon,
|
|
||||||
double? shipping,
|
|
||||||
double? tax,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (currency != null) 'currency': currency,
|
|
||||||
if (value != null) 'value': value,
|
|
||||||
if (transactionId != null) 'transaction_id': transactionId,
|
|
||||||
if (paymentMethod != null) 'payment_method': paymentMethod,
|
|
||||||
if (coupon != null) 'coupon': coupon,
|
|
||||||
if (shipping != null) 'shipping': shipping,
|
|
||||||
if (tax != null) 'tax': tax,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 会话上下文标签 / Session Context Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建会话上下文标签。
|
|
||||||
///
|
|
||||||
/// - [sessionId]: 会话唯一标识。
|
|
||||||
/// - [sessionNumber]: 第几次会话(用户历史会话计数)。
|
|
||||||
/// - [eventIndex]: 当前事件在会话中的序号。
|
|
||||||
/// - [sessionDurationSeconds]: 会话持续时长(秒)。
|
|
||||||
static Map<String, dynamic> session({
|
|
||||||
String? sessionId,
|
|
||||||
int? sessionNumber,
|
|
||||||
int? eventIndex,
|
|
||||||
int? sessionDurationSeconds,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (sessionId != null) 'session_id': sessionId,
|
|
||||||
if (sessionNumber != null) 'session_number': sessionNumber,
|
|
||||||
if (eventIndex != null) 'event_index': eventIndex,
|
|
||||||
if (sessionDurationSeconds != null)
|
|
||||||
'session_duration_seconds': sessionDurationSeconds,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 内容/媒体标签 / Content/Media Tags
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 创建内容/媒体标签。
|
|
||||||
///
|
|
||||||
/// - [contentType]: 内容类型,例如 "video", "article", "audio", "image"。
|
|
||||||
/// - [contentId]: 内容唯一标识。
|
|
||||||
/// - [contentName]: 内容名称/标题。
|
|
||||||
/// - [contentCategory]: 内容分类。
|
|
||||||
/// - [contentDurationSeconds]: 内容时长(秒,适用于视频/音频)。
|
|
||||||
/// - [contentAuthor]: 内容作者/创作者。
|
|
||||||
static Map<String, dynamic> content({
|
|
||||||
String? contentType,
|
|
||||||
String? contentId,
|
|
||||||
String? contentName,
|
|
||||||
String? contentCategory,
|
|
||||||
int? contentDurationSeconds,
|
|
||||||
String? contentAuthor,
|
|
||||||
}) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
if (contentType != null) 'content_type': contentType,
|
|
||||||
if (contentId != null) 'content_id': contentId,
|
|
||||||
if (contentName != null) 'content_name': contentName,
|
|
||||||
if (contentCategory != null) 'content_category': contentCategory,
|
|
||||||
if (contentDurationSeconds != null)
|
|
||||||
'content_duration_seconds': contentDurationSeconds,
|
|
||||||
if (contentAuthor != null) 'content_author': contentAuthor,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 合并工具 / Merge Utility
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// 合并多个标签模板为一个 Map。
|
|
||||||
///
|
|
||||||
/// 后面的模板会覆盖前面的同名键。
|
|
||||||
///
|
|
||||||
/// 示例:
|
|
||||||
/// ```dart
|
|
||||||
/// final tags = TagTemplates.merge([
|
|
||||||
/// TagTemplates.businessContext(tenantId: 't1', appVersion: '1.0.0'),
|
|
||||||
/// TagTemplates.forScreen(screenName: 'HomePage'),
|
|
||||||
/// TagTemplates.campaign(campaignSource: 'google', campaignMedium: 'cpc'),
|
|
||||||
/// ]);
|
|
||||||
/// ```
|
|
||||||
static Map<String, dynamic> merge(List<Map<String, dynamic>> templates) {
|
|
||||||
final result = <String, dynamic>{};
|
|
||||||
for (final template in templates) {
|
|
||||||
result.addAll(template);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,6 @@ export 'src/model/device_info.dart';
|
||||||
export 'src/model/event.dart';
|
export 'src/model/event.dart';
|
||||||
export 'src/model/recent_event_summary.dart';
|
export 'src/model/recent_event_summary.dart';
|
||||||
export 'src/model/user_info.dart';
|
export 'src/model/user_info.dart';
|
||||||
export 'src/util/tag_templates.dart';
|
|
||||||
|
|
||||||
/// 对外唯一入口(Facade)。
|
/// 对外唯一入口(Facade)。
|
||||||
class Analytics {
|
class Analytics {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ environment:
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.22.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
device_info_plus: ^12.3.0
|
|
||||||
dio: ^5.4.3+1
|
dio: ^5.4.3+1
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ AnalyticsConfig _base({
|
||||||
}) {
|
}) {
|
||||||
return AnalyticsConfig(
|
return AnalyticsConfig(
|
||||||
systemCode: systemCode ?? 'SYS',
|
systemCode: systemCode ?? 'SYS',
|
||||||
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com',
|
endpointBaseUrl: endpointBaseUrl ?? 'https://example.com/api',
|
||||||
clientType: clientType ?? 3,
|
clientType: clientType ?? 3,
|
||||||
batchSize: batchSize ?? 20,
|
batchSize: batchSize ?? 20,
|
||||||
flushInterval: flushInterval ?? 15,
|
flushInterval: flushInterval ?? 15,
|
||||||
|
|
@ -49,13 +49,6 @@ 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(),
|
||||||
|
|
@ -130,36 +123,18 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
group('AnalyticsConfig.uri 组装', () {
|
group('AnalyticsConfig.uri 组装', () {
|
||||||
test('基础 host 会拼接 api/ExternalEventlogs 路径', () {
|
test('basePath 为空时会补 /leaf', () {
|
||||||
final config = _base(endpointBaseUrl: 'https://example.com');
|
final config = _base(endpointBaseUrl: 'https://example.com');
|
||||||
expect(
|
expect(config.addEventListLogUri.path, '/AddEventListLog');
|
||||||
config.addEventListLogUri.path,
|
expect(config.addEventLogUri.path, '/AddEventLog');
|
||||||
'/api/ExternalEventlogs/AddEventListLog',
|
expect(config.getSystemAllDimInfoUri.path, '/GetSystemAllDimInfo');
|
||||||
);
|
|
||||||
expect(
|
|
||||||
config.addEventLogUri.path,
|
|
||||||
'/api/ExternalEventlogs/AddEventLog',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
config.getSystemAllDimInfoUri.path,
|
|
||||||
'/api/ExternalEventlogs/GetSystemAllDimInfo',
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('末尾带斜杠的 host 也可正常拼接', () {
|
test('basePath 有值且带斜杠时会规范化', () {
|
||||||
final config = _base(endpointBaseUrl: 'https://example.com/');
|
final config = _base(endpointBaseUrl: 'https://example.com/api/');
|
||||||
expect(
|
expect(config.addEventListLogUri.path, '/api/AddEventListLog');
|
||||||
config.addEventListLogUri.path,
|
expect(config.addEventLogUri.path, '/api/AddEventLog');
|
||||||
'/api/ExternalEventlogs/AddEventListLog',
|
expect(config.getSystemAllDimInfoUri.path, '/api/GetSystemAllDimInfo');
|
||||||
);
|
|
||||||
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',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
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;
|
||||||
final config = AnalyticsConfig(
|
const config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
batchSize: 10,
|
batchSize: 10,
|
||||||
|
|
@ -879,9 +879,9 @@ void main() {
|
||||||
schedulerFactory: _noopScheduler,
|
schedulerFactory: _noopScheduler,
|
||||||
);
|
);
|
||||||
|
|
||||||
final config = AnalyticsConfig(
|
const config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableDebug: true,
|
enableDebug: true,
|
||||||
flushInterval: 3600,
|
flushInterval: 3600,
|
||||||
|
|
@ -1206,9 +1206,9 @@ void main() {
|
||||||
schedulerFactory: _noopScheduler,
|
schedulerFactory: _noopScheduler,
|
||||||
);
|
);
|
||||||
|
|
||||||
final config = AnalyticsConfig(
|
const config = AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
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 AnalyticsConfig(
|
return const 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 AnalyticsConfig(
|
return const AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
enableMetrics: false,
|
enableMetrics: false,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,9 @@ class _NoopScheduler extends Scheduler {
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return const AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
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'),
|
_config('https://example.com/api'),
|
||||||
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'),
|
_config('https://example.com/api'),
|
||||||
httpClientAdapter: adapter,
|
httpClientAdapter: adapter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,9 +38,9 @@ const DeviceInfo _device = DeviceInfo(
|
||||||
);
|
);
|
||||||
|
|
||||||
AnalyticsConfig _config() {
|
AnalyticsConfig _config() {
|
||||||
return AnalyticsConfig(
|
return const AnalyticsConfig(
|
||||||
systemCode: 'TEST_APP',
|
systemCode: 'TEST_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
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 配置通过校验', () {
|
||||||
final config = AnalyticsConfig(
|
const config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'https://example.com',
|
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -15,9 +15,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('非 https 配置会抛错', () {
|
test('非 https 配置会抛错', () {
|
||||||
final config = AnalyticsConfig(
|
const config = AnalyticsConfig(
|
||||||
systemCode: 'OA_APP',
|
systemCode: 'OA_APP',
|
||||||
endpointBaseUrl: 'http://example.com',
|
endpointBaseUrl: 'http://example.com/api/ExternalEventlogs',
|
||||||
clientType: 3,
|
clientType: 3,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue