feat: 更新请求地址
Flutter CI / analyze-and-test (push) Waiting to run Details

This commit is contained in:
Max 2026-02-03 15:00:29 +08:00
parent 4cc822c4f6
commit 745ab3510e
14 changed files with 472 additions and 50 deletions

45
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

View File

@ -12,7 +12,7 @@
- `POST /api/ExternalEventlogs/AddEventListLog`
- `POST /api/ExternalEventlogs/AddEventLog`
需要基于上述接口,开发一个独立的 Flutter 埋点 SDK纯 Dart 实现),并规划三阶段能力演进。
需要基于上述接口,开发一个独立的 Flutter 埋点 SDKFlutter 运行环境使用,非纯 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), // 事件过期时间
});
}

View File

@ -74,6 +74,7 @@
- [x] 已接入 `GET /GetSystemAllDimInfo?system_code=...`
- [x] 已在 `AnalyticsCore.init` 中异步拉取(失败不影响埋点)
- [x] 周期刷新已补齐(定时器按 refreshInterval 拉取)
- [x] Example 联调支持 HTTP新增 allowInsecureHttp 配置)
### 3. Validator事件校验

View File

@ -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 按照以上方案执行修改。**

View File

@ -1,6 +1,6 @@
# yx_tracking_flutter
企业级 Flutter 埋点 SDK纯 Dart 实现),对齐后端接口:
企业级 Flutter 埋点 SDKFlutter 运行环境使用,非纯 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`

View File

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

View File

@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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