feat: 更新请求地址
Flutter CI / analyze-and-test (push) Waiting to run
Details
Flutter CI / analyze-and-test (push) Waiting to run
Details
This commit is contained in:
parent
4cc822c4f6
commit
745ab3510e
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
// 使用 IntelliSense 了解相关属性。
|
||||
// 悬停以查看现有属性的描述。
|
||||
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "yx_tracking_flutter",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "yx_tracking_flutter (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "yx_tracking_flutter (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
},
|
||||
{
|
||||
"name": "example",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "example (profile mode)",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "example (release mode)",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
- `POST /api/ExternalEventlogs/AddEventListLog`
|
||||
- `POST /api/ExternalEventlogs/AddEventLog`
|
||||
|
||||
需要基于上述接口,开发一个独立的 Flutter 埋点 SDK(纯 Dart 实现),并规划三阶段能力演进。
|
||||
需要基于上述接口,开发一个独立的 Flutter 埋点 SDK(Flutter 运行环境使用,非纯 Dart 运行时),并规划三阶段能力演进。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ class AnalyticsConfig {
|
|||
this.maxRetryCount = 3,
|
||||
this.connectTimeout = const Duration(seconds: 5),
|
||||
this.readTimeout = const Duration(seconds: 5),
|
||||
this.useIsolateStorage = true, // 默认开启 Isolate
|
||||
this.useIsolateStorage = true, // 默认开启 Isolate(仅 Flutter 环境可用)
|
||||
this.maxEventAge = const Duration(days: 7), // 事件过期时间
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@
|
|||
- [x] 已接入 `GET /GetSystemAllDimInfo?system_code=...`
|
||||
- [x] 已在 `AnalyticsCore.init` 中异步拉取(失败不影响埋点)
|
||||
- [x] 周期刷新已补齐(定时器按 refreshInterval 拉取)
|
||||
- [x] Example 联调支持 HTTP(新增 allowInsecureHttp 配置)
|
||||
|
||||
### 3. Validator(事件校验)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
# Example App 集成实施方案
|
||||
# Example App Integration Implementation Plan
|
||||
|
||||
> **目标 (Goal)**: 使 example app 能够完整运行并对接真实后端
|
||||
> **执行者 (Executor)**: Codex
|
||||
> **日期 (Date)**: 2026-01-28
|
||||
> **执行记录 (Execution)**: 已执行(参数名修复、Example 配置更新、生命周期监听接入、HTTP 放行)
|
||||
|
||||
---
|
||||
|
||||
## 一、后端配置信息 | Backend Configuration
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|---|
|
||||
| **API 基础地址** | `http://192.168.2.7:18828/api/ExternalEventlogs` |
|
||||
| **系统标识** | `SDK-TEST-FLUTTER` |
|
||||
| **系统名称** | Flutter SDK测试 |
|
||||
| **认证方式** | 无需认证 |
|
||||
| **协议** | HTTP(生产建议 HTTPS) |
|
||||
|
||||
---
|
||||
|
||||
## 二、需要修改的文件 | Files to Modify
|
||||
|
||||
### 2.1 修复参数命名问题
|
||||
|
||||
#### [MODIFY] `lib/src/config/config_manager.dart`
|
||||
|
||||
**问题**: SDK 使用 `system_code`,但后端期望 `systemCode`
|
||||
|
||||
**修改位置**: 第 59 行
|
||||
|
||||
```diff
|
||||
- queryParameters: <String, dynamic>{'system_code': config.systemCode},
|
||||
+ queryParameters: <String, dynamic>{'systemCode': config.systemCode},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 更新 Example App 配置
|
||||
|
||||
#### [MODIFY] `example/lib/main.dart`
|
||||
|
||||
**修改位置**: 第 10-17 行
|
||||
|
||||
```diff
|
||||
await Analytics.init(
|
||||
const AnalyticsConfig(
|
||||
- systemCode: 'DEMO_APP',
|
||||
- endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||
+ systemCode: 'SDK-TEST-FLUTTER',
|
||||
+ endpointBaseUrl: 'http://192.168.2.7:18828/api/ExternalEventlogs',
|
||||
clientType: 3,
|
||||
enableDebug: true,
|
||||
batchSize: 5,
|
||||
flushInterval: 30,
|
||||
allowInsecureHttp: true,
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
**修改位置**: 第 161-164 行(更新说明文字)
|
||||
|
||||
```diff
|
||||
- const Text(
|
||||
- '说明:Demo 使用 example.com 作为占位地址,'
|
||||
- '实际联调时请替换为真实 HTTPS 域名。',
|
||||
- ),
|
||||
+ const Text(
|
||||
+ '说明:已对接 SDK-TEST-FLUTTER 系统,'
|
||||
+ '点击 Track 按钮记录事件,点击 Flush 上报。',
|
||||
+ ),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 添加生命周期监听(可选优化)
|
||||
|
||||
#### [MODIFY] `example/lib/main.dart`
|
||||
|
||||
**修改位置**: 第 18 行之后添加
|
||||
|
||||
```dart
|
||||
// 绑定生命周期监听,后台时自动 flush
|
||||
Analytics.bindLifecycleObserver();
|
||||
|
||||
---
|
||||
|
||||
### 2.4 放行 HTTP(开发/测试环境)
|
||||
|
||||
#### [MODIFY] `lib/src/config/analytics_config.dart`
|
||||
|
||||
**说明**: 为联调环境允许 HTTP,新增 `allowInsecureHttp` 配置项。
|
||||
|
||||
```diff
|
||||
const AnalyticsConfig({
|
||||
...
|
||||
this.allowInsecureHttp = false,
|
||||
});
|
||||
|
||||
// validate()
|
||||
if (scheme == 'http' && allowInsecureHttp) {
|
||||
// 允许开发/测试环境使用 HTTP
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、验证步骤 | Verification Steps
|
||||
|
||||
### 3.1 运行测试
|
||||
|
||||
```bash
|
||||
cd /Volumes/Workspace/SourceCode/yuanxuan/yx_tracking_flutter
|
||||
flutter test
|
||||
```
|
||||
|
||||
### 3.2 运行 Example App
|
||||
|
||||
```bash
|
||||
cd /Volumes/Workspace/SourceCode/yuanxuan/yx_tracking_flutter/example
|
||||
flutter run -d macos # 或 chrome / android / ios
|
||||
```
|
||||
|
||||
### 3.3 功能验证清单
|
||||
|
||||
- [ ] 启动 App 无报错
|
||||
- [ ] 点击 "Track Demo Event",缓存数量 +1
|
||||
- [ ] 点击 "Flush Now",事件成功上报(HTTP 200)
|
||||
- [ ] 点击 "Refresh Config",从后端拉取配置成功
|
||||
- [ ] 在管理后台查看上报的事件数据
|
||||
|
||||
---
|
||||
|
||||
## 四、API 接口参考 | API Reference
|
||||
|
||||
### 4.1 事件上报
|
||||
|
||||
```http
|
||||
POST /api/ExternalEventlogs/AddEventListLog
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"system_code": "SDK-TEST-FLUTTER",
|
||||
"eventType": "DEMO_BUTTON_CLICK",
|
||||
"userInfo": null,
|
||||
"clientType": 3,
|
||||
"clientTimestamp": 1706000000000,
|
||||
"timestamp": "2026-01-28T12:00:00.000Z",
|
||||
"deviceInfo": {
|
||||
"os": "macOS 14.0",
|
||||
"model": "MacBook Pro",
|
||||
"screenResolution": "1920x1080"
|
||||
},
|
||||
"eventParams": {"page": "demo"},
|
||||
"customTags": {"tenantId": "t1", "feature": "demo"}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 4.2 获取配置
|
||||
|
||||
```http
|
||||
GET /api/ExternalEventlogs/GetSystemAllDimInfo?systemCode=SDK-TEST-FLUTTER
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"systemInfo": {
|
||||
"system_code": "SDK-TEST-FLUTTER",
|
||||
"system_name": "Flutter SDK测试"
|
||||
},
|
||||
"systemEventTypes": [
|
||||
{"event_code": "DEMO_BUTTON_CLICK", "event_name": "Demo按钮点击"}
|
||||
],
|
||||
"systemCustonTas": [
|
||||
{"tag_name": "tenantId", "tag_type": "string"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型对齐 | Data Model Alignment
|
||||
|
||||
| 后端字段 (EventlogAddDto) | SDK 字段 (Event) | 状态 |
|
||||
|---------------------------|------------------|:----:|
|
||||
| `system_code` | `systemCode` | ✅ |
|
||||
| `eventType` | `eventType` | ✅ |
|
||||
| `userInfo` | `userInfo` | ✅ |
|
||||
| `clientType` (1=Android, 2=iOS, 3=Flutter) | `clientType` | ✅ 使用 3 |
|
||||
| `clientTimestamp` (int64 毫秒) | `clientTimestamp` | ✅ |
|
||||
| `timestamp` (ISO8601) | `timestamp` | ✅ |
|
||||
| `deviceInfo` | `deviceInfo` | ✅ |
|
||||
| `eventParams` | `eventParams` | ✅ |
|
||||
| `customTags` | `customTags` | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 六、注意事项 | Notes
|
||||
|
||||
1. **网络环境**: 确保开发机器可访问 `192.168.2.7:18828`
|
||||
2. **事件类型**: 如需校验事件类型,需在管理后台配置 `DEMO_BUTTON_CLICK`
|
||||
3. **自定义标签**: 如需校验标签,需在管理后台配置 `tenantId`、`feature`
|
||||
4. **HTTP 仅限联调**: 生产环境请务必使用 HTTPS,并关闭 `allowInsecureHttp`
|
||||
|
||||
---
|
||||
|
||||
**请 Codex 按照以上方案执行修改。**
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# yx_tracking_flutter
|
||||
|
||||
企业级 Flutter 埋点 SDK(纯 Dart 实现),对齐后端接口:
|
||||
企业级 Flutter 埋点 SDK(Flutter 运行环境使用,非纯 Dart 运行时),对齐后端接口:
|
||||
|
||||
- `GET /api/ExternalEventlogs/GetSystemAllDimInfo`
|
||||
- `POST /api/ExternalEventlogs/AddEventListLog`
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
## 功能特性
|
||||
|
||||
- 统一事件模型,自动补齐公共字段(systemCode、deviceInfo、时间戳等)
|
||||
- 本地持久化队列(sqflite),默认运行在独立 Isolate 中,避免阻塞 UI
|
||||
- 本地持久化队列(sqflite),默认运行在独立 Isolate 中,避免阻塞 UI(需 Flutter 环境)
|
||||
- 应用生命周期监听(进入后台/销毁时自动 flush)
|
||||
- 批量上报 + 重试退避 + 队列上限裁剪
|
||||
- 配置下发(`GetSystemAllDimInfo`)与本地缓存
|
||||
|
|
@ -61,7 +61,8 @@ await Analytics.flush(force: true);
|
|||
## 关键配置项(AnalyticsConfig)
|
||||
|
||||
除了文档中的 Phase 1 配置项,还新增了以下能力配置:
|
||||
- `useIsolateStorage`: 是否使用 Isolate 进行存储操作(默认 `true`,强烈建议开启)
|
||||
- `useIsolateStorage`: 是否使用 Isolate 进行存储操作(默认 `true`,强烈建议在 Flutter 环境开启)
|
||||
- `allowInsecureHttp`:是否允许使用 HTTP(默认 `false`,仅用于开发/测试环境)
|
||||
- `enableMetrics`:是否启用 SDK 自监控指标(默认 `true`)
|
||||
- `metricsReportInterval`:指标上报周期(默认 10 分钟)
|
||||
- `blockOnValidationError`:Debug 下遇到校验 error 是否阻断发送(默认 `false`)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
10C79C360DE55F92A491C2EB /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D0325B7C115751406A2CCA7 /* Pods_RunnerTests.framework */; };
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B82F0DBA26E4C6FF84DE80BC /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8CB685FF82E44B570FFF566 /* Pods_Runner.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
|
|
@ -40,11 +42,14 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
127F9D35E6FC15E57EB026A7 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
561FD5B8376A263AABDC43F7 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6D0325B7C115751406A2CCA7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
|
|
@ -55,13 +60,27 @@
|
|||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
99B0C1196048B43435272445 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
A5F818923BFCE77BE58D099D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
CFFBDFA0A7DFEC84FAF6C9E5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
F043EE09D6AAF3802C8BF291 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
F8CB685FF82E44B570FFF566 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
0055AA437E8F768D68AD33FE /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
10C79C360DE55F92A491C2EB /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B82F0DBA26E4C6FF84DE80BC /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -76,6 +95,15 @@
|
|||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
56450A33E60164813EC2B769 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F8CB685FF82E44B570FFF566 /* Pods_Runner.framework */,
|
||||
6D0325B7C115751406A2CCA7 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -94,6 +122,8 @@
|
|||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
F946EF3DDF6321BC391B8495 /* Pods */,
|
||||
56450A33E60164813EC2B769 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -121,6 +151,20 @@
|
|||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F946EF3DDF6321BC391B8495 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5F818923BFCE77BE58D099D /* Pods-Runner.debug.xcconfig */,
|
||||
561FD5B8376A263AABDC43F7 /* Pods-Runner.release.xcconfig */,
|
||||
99B0C1196048B43435272445 /* Pods-Runner.profile.xcconfig */,
|
||||
F043EE09D6AAF3802C8BF291 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
CFFBDFA0A7DFEC84FAF6C9E5 /* Pods-RunnerTests.release.xcconfig */,
|
||||
127F9D35E6FC15E57EB026A7 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -128,8 +172,10 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
61DF17F1CEED61F4D4BCEAE3 /* [CP] Check Pods Manifest.lock */,
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
0055AA437E8F768D68AD33FE /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -145,12 +191,14 @@
|
|||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
98678C9D0961C6E0D7564335 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
E75446E1D1B4687890B630C3 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
|
@ -238,6 +286,28 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
61DF17F1CEED61F4D4BCEAE3 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
|
|
@ -253,6 +323,45 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
98678C9D0961C6E0D7564335 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
E75446E1D1B4687890B630C3 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
|
@ -379,6 +488,7 @@
|
|||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F043EE09D6AAF3802C8BF291 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -396,6 +506,7 @@
|
|||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = CFFBDFA0A7DFEC84FAF6C9E5 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
@ -411,6 +522,7 @@
|
|||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 127F9D35E6FC15E57EB026A7 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
|
|
|||
|
|
@ -4,4 +4,7 @@
|
|||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
|
|||
|
|
@ -8,15 +8,18 @@ Future<void> main() async {
|
|||
|
||||
await Analytics.init(
|
||||
const AnalyticsConfig(
|
||||
systemCode: 'DEMO_APP',
|
||||
endpointBaseUrl: 'https://example.com/api/ExternalEventlogs',
|
||||
systemCode: 'SDK-TEST-FLUTTER',
|
||||
endpointBaseUrl: 'http://192.168.2.7:18828/api/',
|
||||
clientType: 3,
|
||||
enableDebug: true,
|
||||
batchSize: 5,
|
||||
flushInterval: 30,
|
||||
allowInsecureHttp: true,
|
||||
),
|
||||
);
|
||||
|
||||
Analytics.bindLifecycleObserver();
|
||||
|
||||
runApp(const DemoApp());
|
||||
}
|
||||
|
||||
|
|
@ -25,9 +28,7 @@ class DemoApp extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: DemoPage(),
|
||||
);
|
||||
return const MaterialApp(home: DemoPage());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,10 +81,7 @@ class _DemoPageState extends State<DemoPage> {
|
|||
await Analytics.track(
|
||||
'DEMO_BUTTON_CLICK',
|
||||
eventParams: const <String, dynamic>{'page': 'demo'},
|
||||
customTags: const <String, dynamic>{
|
||||
'tenantId': 't1',
|
||||
'feature': 'demo',
|
||||
},
|
||||
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||||
);
|
||||
await _refreshCount();
|
||||
}
|
||||
|
|
@ -153,17 +151,22 @@ class _DemoPageState extends State<DemoPage> {
|
|||
),
|
||||
OutlinedButton(
|
||||
onPressed: _refreshingConfig ? null : _refreshConfig,
|
||||
child: Text(_refreshingConfig ? 'Refreshing...' : 'Refresh Config'),
|
||||
child: Text(
|
||||
_refreshingConfig ? 'Refreshing...' : 'Refresh Config',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'说明:Demo 使用 example.com 作为占位地址,'
|
||||
'实际联调时请替换为真实 HTTPS 域名。',
|
||||
'说明:已对接 SDK-TEST-FLUTTER 系统,'
|
||||
'点击 Track 按钮记录事件,点击 Flush 上报。',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text('最近事件(最多 20 条)', style: Theme.of(context).textTheme.titleSmall),
|
||||
Text(
|
||||
'最近事件(最多 20 条)',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: _recent.isEmpty
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class AnalyticsConfig {
|
|||
this.readTimeout = const Duration(seconds: 5),
|
||||
this.maxEventAge = const Duration(days: 7),
|
||||
this.useIsolateStorage = true,
|
||||
this.allowInsecureHttp = false,
|
||||
this.enableMetrics = true,
|
||||
this.metricsReportInterval = const Duration(minutes: 10),
|
||||
this.blockOnValidationError = false,
|
||||
|
|
@ -57,6 +58,9 @@ class AnalyticsConfig {
|
|||
/// 是否使用 Isolate 执行存储操作。
|
||||
final bool useIsolateStorage;
|
||||
|
||||
/// 是否允许 HTTP(仅用于开发/测试环境)。
|
||||
final bool allowInsecureHttp;
|
||||
|
||||
/// 是否开启 SDK 指标上报。
|
||||
final bool enableMetrics;
|
||||
|
||||
|
|
@ -82,13 +86,18 @@ class AnalyticsConfig {
|
|||
'endpointBaseUrl 不是合法的 URL',
|
||||
);
|
||||
}
|
||||
if (uri.scheme.toLowerCase() != 'https') {
|
||||
final scheme = uri.scheme.toLowerCase();
|
||||
if (scheme != 'https') {
|
||||
if (scheme == 'http' && allowInsecureHttp) {
|
||||
// 允许开发/测试环境使用 HTTP。
|
||||
} else {
|
||||
throw ArgumentError.value(
|
||||
endpointBaseUrl,
|
||||
'endpointBaseUrl',
|
||||
'endpointBaseUrl 必须使用 HTTPS',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (clientType <= 0) {
|
||||
throw ArgumentError.value(
|
||||
|
|
@ -156,13 +165,15 @@ class AnalyticsConfig {
|
|||
}
|
||||
|
||||
/// `/AddEventListLog` 的完整地址。
|
||||
Uri get addEventListLogUri => _appendPath('AddEventListLog');
|
||||
Uri get addEventListLogUri =>
|
||||
_appendPath('ExternalEventlogs/AddEventListLog');
|
||||
|
||||
/// `/AddEventLog` 的完整地址(可选降级)。
|
||||
Uri get addEventLogUri => _appendPath('AddEventLog');
|
||||
Uri get addEventLogUri => _appendPath('ExternalEventlogs/AddEventLog');
|
||||
|
||||
/// `/GetSystemAllDimInfo` 的完整地址(Phase 2+)。
|
||||
Uri get getSystemAllDimInfoUri => _appendPath('GetSystemAllDimInfo');
|
||||
Uri get getSystemAllDimInfoUri =>
|
||||
_appendPath('ExternalEventlogs/GetSystemAllDimInfo');
|
||||
|
||||
Uri _appendPath(String leaf) {
|
||||
final base = Uri.parse(endpointBaseUrl);
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@ class ConfigManager {
|
|||
|
||||
try {
|
||||
final response = await _httpClient.get<dynamic>(
|
||||
'GetSystemAllDimInfo',
|
||||
queryParameters: <String, dynamic>{'system_code': config.systemCode},
|
||||
config.getSystemAllDimInfoUri.toString(),
|
||||
queryParameters: <String, dynamic>{'systemCode': config.systemCode},
|
||||
);
|
||||
|
||||
final payload = _extractPayloadMap(response.data);
|
||||
|
|
@ -116,7 +116,8 @@ class ConfigManager {
|
|||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
Logger.warn('配置响应结构无法识别,已忽略');
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _looksLikeDimInfo(Map<String, dynamic> map) {
|
||||
|
|
|
|||
|
|
@ -114,10 +114,19 @@ class AnalyticsCore {
|
|||
Logger.info('AnalyticsCore 初始化开始');
|
||||
|
||||
final deviceInfo = await _deviceInfoCollector();
|
||||
final storage = _useDefaultStorageFactory
|
||||
? _buildDefaultStorage(config)
|
||||
: _storageFactory();
|
||||
EventStorage storage =
|
||||
_useDefaultStorageFactory ? _buildDefaultStorage(config) : _storageFactory();
|
||||
try {
|
||||
await storage.init();
|
||||
} on Object catch (e, st) {
|
||||
if (_useDefaultStorageFactory && config.useIsolateStorage) {
|
||||
Logger.error('Isolate 存储初始化失败,降级为默认存储', e, st);
|
||||
storage = SqfliteEventStorage();
|
||||
await storage.init();
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
final configManager = _configManagerFactory(config);
|
||||
await configManager.init();
|
||||
|
||||
|
|
|
|||
|
|
@ -39,11 +39,11 @@ class ApiClient {
|
|||
ApiClient(
|
||||
AnalyticsConfig config, {
|
||||
HttpClient? httpClient,
|
||||
}) : _httpClient = httpClient ?? HttpClient(config);
|
||||
|
||||
static const String _addEventListLogPath = 'AddEventListLog';
|
||||
}) : _httpClient = httpClient ?? HttpClient(config),
|
||||
_config = config;
|
||||
|
||||
final HttpClient _httpClient;
|
||||
final AnalyticsConfig _config;
|
||||
|
||||
/// 批量发送事件。
|
||||
///
|
||||
|
|
@ -57,7 +57,7 @@ class ApiClient {
|
|||
|
||||
try {
|
||||
final response = await _httpClient.post<dynamic>(
|
||||
_addEventListLogPath,
|
||||
_config.addEventListLogUri.toString(),
|
||||
data: payload,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
|
|||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'package:yx_tracking_flutter/src/config/analytics_config.dart';
|
||||
import 'package:yx_tracking_flutter/src/util/logger.dart';
|
||||
|
||||
/// 对 Dio 的轻量封装,统一超时与基础配置。
|
||||
class HttpClient {
|
||||
|
|
@ -9,8 +10,7 @@ class HttpClient {
|
|||
HttpClient(
|
||||
AnalyticsConfig config, {
|
||||
HttpClientAdapter? httpClientAdapter,
|
||||
})
|
||||
: _dio = Dio(
|
||||
}) : _dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: _normalizeBaseUrl(config.endpointBaseUrl),
|
||||
connectTimeout: config.connectTimeout,
|
||||
|
|
@ -38,13 +38,29 @@ class HttpClient {
|
|||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, Object?>? headers,
|
||||
CancelToken? cancelToken,
|
||||
}) =>
|
||||
_dio.get<T>(
|
||||
path,
|
||||
}) {
|
||||
// 如果 path 是完整 URL(以 http 开头),则直接使用,否则基于 baseUrl 拼接。
|
||||
final isAbsolutePath = path.toLowerCase().startsWith('http');
|
||||
final String requestPath = isAbsolutePath ? path : path;
|
||||
|
||||
// 日志
|
||||
if (isAbsolutePath) {
|
||||
Logger.info('GET Request: $requestPath query=$queryParameters');
|
||||
} else {
|
||||
final baseUrl = _dio.options.baseUrl;
|
||||
final fullUri = Uri.parse(baseUrl)
|
||||
.resolve(path)
|
||||
.replace(queryParameters: queryParameters);
|
||||
Logger.info('GET Request: $fullUri');
|
||||
}
|
||||
|
||||
return _dio.get<T>(
|
||||
requestPath,
|
||||
queryParameters: queryParameters,
|
||||
options: _withHeaders(headers),
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// 发送 POST 请求。
|
||||
Future<Response<T>> post<T>(
|
||||
|
|
@ -70,9 +86,6 @@ class HttpClient {
|
|||
}
|
||||
|
||||
static String _normalizeBaseUrl(String baseUrl) {
|
||||
if (baseUrl.endsWith('/')) {
|
||||
return baseUrl.substring(0, baseUrl.length - 1);
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ AnalyticsConfig _base({
|
|||
Duration? readTimeout,
|
||||
Duration? maxEventAge,
|
||||
bool? useIsolateStorage,
|
||||
bool? allowInsecureHttp,
|
||||
bool? enableMetrics,
|
||||
Duration? metricsReportInterval,
|
||||
}) {
|
||||
|
|
@ -28,6 +29,7 @@ AnalyticsConfig _base({
|
|||
readTimeout: readTimeout ?? const Duration(seconds: 1),
|
||||
maxEventAge: maxEventAge ?? const Duration(days: 7),
|
||||
useIsolateStorage: useIsolateStorage ?? false,
|
||||
allowInsecureHttp: allowInsecureHttp ?? false,
|
||||
enableMetrics: enableMetrics ?? true,
|
||||
metricsReportInterval:
|
||||
metricsReportInterval ?? const Duration(minutes: 1),
|
||||
|
|
@ -54,6 +56,15 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('允许 http 时不抛错', () {
|
||||
expect(
|
||||
() =>
|
||||
_base(endpointBaseUrl: 'http://example.com', allowInsecureHttp: true)
|
||||
.validate(),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
|
||||
test('clientType 必须为正数', () {
|
||||
expect(() => _base(clientType: 0).validate(), throwsArgumentError);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue