1028 lines
32 KiB
Markdown
1028 lines
32 KiB
Markdown
# Analytics SDK 使用文档
|
||
|
||
## 目录
|
||
|
||
1. [概述](#1-概述)
|
||
2. [安装](#2-安装)
|
||
3. [SDK 初始化](#3-sdk-初始化)
|
||
4. [用户管理](#4-用户管理)
|
||
5. [自动埋点](#5-自动埋点)
|
||
6. [手动事件上报](#6-手动事件上报)
|
||
7. [枚举参考](#7-枚举参考)
|
||
8. [SdkConfig 全局参数](#8-sdkconfig-全局参数)
|
||
9. [上报数据格式](#9-上报数据格式)
|
||
10. [调试工具](#10-调试工具)
|
||
11. [高级用法](#11-高级用法)
|
||
12. [完整集成示例](#12-完整集成示例)
|
||
|
||
---
|
||
|
||
## 1. 概述
|
||
|
||
Analytics SDK 是一个 Flutter 插件,提供:
|
||
|
||
- 自动埋点:页面浏览、页面生命周期、全局点击、导航事件(均默认开启)
|
||
- 手动埋点:视频/小说/漫画行为、广告、推荐等客户端事件
|
||
- 本地持久化缓存(JSONL 格式),断网重传
|
||
- 域名并发测速,自动选择最快上报节点
|
||
- AES-GCM 加密配置,防止配置明文泄露
|
||
- 事件类型白名单控制(服务端下发)
|
||
- 广告展示去重(session 级别)
|
||
- 设备信息自动采集(通过 `device_info_plus`,无需外部传入)
|
||
- 设备指纹自动生成
|
||
- `getParams()` 方法暴露全部或指定公共参数,供服务端复用
|
||
|
||
所有公开方法均**不会向外抛出异常**,SDK 内部失败不影响宿主应用运行。
|
||
|
||
---
|
||
|
||
## 2. 安装
|
||
|
||
在 `pubspec.yaml` 中添加依赖:
|
||
|
||
```yaml
|
||
dependencies:
|
||
analytics_sdk:
|
||
path: ../analytics_sdk # 或 git/pub 地址
|
||
```
|
||
|
||
运行:
|
||
|
||
```bash
|
||
flutter pub get
|
||
```
|
||
|
||
---
|
||
|
||
## 3. SDK 初始化
|
||
|
||
### 3.1 最简初始化
|
||
|
||
```dart
|
||
import 'package:analytics_sdk/analytics_sdk.dart';
|
||
|
||
await AnalyticsSdk.instance.init(
|
||
appId: 'your_app_id',
|
||
encryptedConfig: 'Base64加密字符串', // 服务端下发
|
||
deviceId: 'your_device_id', // Android IMEI/OAID、iOS IDFV 等
|
||
);
|
||
```
|
||
|
||
设备品牌、型号、系统名称、版本等信息由 SDK 内部通过 `device_info_plus` 自动采集。
|
||
|
||
### 3.2 完整参数说明
|
||
|
||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||
|------|------|------|--------|------|
|
||
| `appId` | `String` | 是 | — | 应用 ID |
|
||
| `encryptedConfig` | `String?` | 是(可传 null) | null | 加密配置字符串,见下方格式说明 |
|
||
| `channel` | `String?` | 否 | '' | 渠道标识,可后续通过 `setChannel` 更新 |
|
||
| `uid` | `String?` | 否 | '' | 用户 ID,未登录可传空,登录后通过 `setUserIdAndType` 更新 |
|
||
| `deviceId` | `String` | 是 | — | 设备唯一标识(Android IMEI/OAID、iOS IDFV 等) |
|
||
| `enableDebugBanner` | `bool` | 否 | `false` | 是否显示调试悬浮条(仅在非 release 包生效) |
|
||
|
||
> **自动采集字段**:`device`(类型)、`device_brand`、`device_model`、`system_name`、`system_version`、`user_agent` 均由 SDK 通过 `device_info_plus` 自动读取,无需传入。
|
||
|
||
### 3.3 encryptedConfig 格式说明
|
||
|
||
`encryptedConfig` 是一个 Base64 编码的 AES-GCM 密文,解密后内容为:
|
||
|
||
```json
|
||
{
|
||
"domainList": [
|
||
"https://api1.example.com",
|
||
"https://api2.example.com"
|
||
],
|
||
"eventList": [
|
||
"app_page_view",
|
||
"page_click",
|
||
"video_event",
|
||
"ad_impression"
|
||
]
|
||
}
|
||
```
|
||
|
||
- `domainList`:上报域名列表,SDK 启动时会并发测速,选择最快的域名上报。
|
||
- `eventList`:启用的事件类型白名单。**不在列表中的事件会被静默丢弃**。传空列表或省略此字段时,所有事件类型均允许上报。
|
||
|
||
如需在调试包中验证加密串,使用 [`validateEncryptedConfig`](#validateencryptedconfig)。
|
||
|
||
### 3.4 初始化时机
|
||
|
||
建议在 `main()` 中 `runApp()` 之前调用:
|
||
|
||
```dart
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
// 可选:在 init() 前覆盖默认参数
|
||
SdkConfig.configure(
|
||
uploadInterval: const Duration(seconds: 5),
|
||
autoUploadThreshold: 10,
|
||
);
|
||
|
||
await AnalyticsSdk.instance.init(
|
||
appId: 'your_app_id',
|
||
encryptedConfig: await fetchEncryptedConfigFromServer(),
|
||
deviceId: await getDeviceId(),
|
||
channel: 'google_play',
|
||
uid: currentUser?.id ?? '',
|
||
);
|
||
|
||
runApp(const MyApp());
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 用户管理
|
||
|
||
### 4.1 登录 / 切换账号
|
||
|
||
```dart
|
||
AnalyticsSdk.setUserIdAndType(
|
||
userId: '12345',
|
||
userTypeEnum: UserTypeEnum.vip,
|
||
);
|
||
```
|
||
|
||
| 参数 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `userId` | `String` | 用户 ID,默认空字符串(未登录状态) |
|
||
| `userTypeEnum` | `UserTypeEnum?` | 用户类型,null 时默认为 `normal` |
|
||
|
||
### 4.2 仅更新 uid(不改变用户类型)
|
||
|
||
```dart
|
||
AnalyticsSdk.setUid('12345');
|
||
```
|
||
|
||
### 4.3 更新渠道
|
||
|
||
```dart
|
||
AnalyticsSdk.setChannel('huawei_store');
|
||
```
|
||
|
||
### 4.4 登出
|
||
|
||
```dart
|
||
AnalyticsSdk.logoutUser();
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 自动埋点
|
||
|
||
### 5.1 页面生命周期自动追踪(PageLifecycleObserver)
|
||
|
||
```dart
|
||
MaterialApp(
|
||
navigatorObservers: [
|
||
AnalyticsSdk.instance.pageObserver,
|
||
],
|
||
)
|
||
```
|
||
|
||
注册后 SDK 会自动追踪页面进入、离开和导航事件,无需手动上报。
|
||
|
||
获取当前页面 key:
|
||
|
||
```dart
|
||
final currentPage = PageLifecycleObserver.currentPageKey;
|
||
```
|
||
|
||
### 5.2 页面名称映射(PageNameMapper)
|
||
|
||
```dart
|
||
import 'package:analytics_sdk/manager/page_name_manager.dart';
|
||
|
||
PageNameMapper.addMappings({
|
||
'home': '首页',
|
||
'video_detail': '视频详情页',
|
||
'user_center': '个人中心',
|
||
'search': '搜索页',
|
||
});
|
||
|
||
final name = PageNameMapper.getPageName('video_detail'); // '视频详情页'
|
||
```
|
||
|
||
> **注意**:`pageKey` 和映射 key 均会自动去除前导 `/`,传 `'/video_detail'` 与 `'video_detail'` 效果相同。
|
||
|
||
### 5.3 全局点击自动追踪(GlobalClickWrapper)
|
||
|
||
```dart
|
||
import 'package:analytics_sdk/widget/global_click_wrapper.dart';
|
||
|
||
MaterialApp(
|
||
builder: (context, child) => GlobalClickWrapper(child: child!),
|
||
)
|
||
```
|
||
|
||
自动上报 `PageClickEvent`,含归一化坐标(300ms 节流)。
|
||
|
||
---
|
||
|
||
## 6. 手动事件上报
|
||
|
||
所有手动事件通过 `AnalyticsSdk.instance.track(event)` 上报。
|
||
|
||
### 6.1 App 安装事件【客户端】
|
||
|
||
> **触发时机**:首次安装启动后,读取剪贴板获取到 traceId 时上报。
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(AppInstallEvent(
|
||
traceId: 'clipboard_trace_id',
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `traceId` | `String` | 落地页生成并写入剪贴板的唯一追踪 ID |
|
||
|
||
---
|
||
|
||
### 6.2 视频行为事件【客户端】
|
||
|
||
> **触发时机**:用户播放、暂停、快进、完播等操作时上报。
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(VideoEvent(
|
||
videoId: 'vid_001',
|
||
videoTitle: '精彩大片',
|
||
videoTypeId: 'action',
|
||
videoTypeName: '动作片',
|
||
videoTagKey: 'hot,new',
|
||
videoTagName: '热门,最新',
|
||
videoDuration: 7200,
|
||
playDuration: 120,
|
||
playProgress: 30,
|
||
videoBehavior: VideoEventEnum.VIDEO_PLAY,
|
||
videoContentType: VideoContentTypeEnum.video,
|
||
recommendTraceId: 'trace_abc123', // 未接推荐引擎传 ''
|
||
mediaId: '',
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `videoId` | `String` | 是 | 视频 ID |
|
||
| `videoTitle` | `String` | 是 | 视频标题 |
|
||
| `videoTypeId` | `String` | 是 | 视频分类 ID |
|
||
| `videoTypeName` | `String` | 是 | 视频分类名称 |
|
||
| `videoTagKey` | `String` | 是 | 标签 KEY,多个用英文逗号分隔 |
|
||
| `videoTagName` | `String` | 是 | 标签名称,多个用英文逗号分隔 |
|
||
| `videoDuration` | `int` | 是 | 视频总时长(秒) |
|
||
| `playDuration` | `int` | 是 | 本次播放时长(秒) |
|
||
| `playProgress` | `int` | 是 | 播放进度百分比(0-100) |
|
||
| `videoBehavior` | `VideoEventEnum` | 是 | 视频行为,见 [VideoEventEnum](#videoeventenum) |
|
||
| `videoContentType` | `VideoContentTypeEnum?` | 否 | 内容类型:`video`(长视频) / `short_video`(短视频),未传时上报空字符串 |
|
||
| `recommendTraceId` | `String` | 否 | 推荐引擎 trace ID,未接推荐引擎传 `''`,默认 `''` |
|
||
| `mediaId` | `String` | 否 | 媒体资源 ID,默认 `''` |
|
||
|
||
---
|
||
|
||
### 6.3 小说阅读事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(NovelEvent(
|
||
novelId: 'novel_001',
|
||
novelTitle: '斗破苍穹',
|
||
novelTypeId: 'fantasy',
|
||
novelTypeName: '玄幻',
|
||
novelTagKey: 'hot,classic',
|
||
novelTagName: '热门,经典',
|
||
readProgress: 45,
|
||
pageNo: 10,
|
||
novelBehavior: ReadBehaviorEnum.PAGE_NEXT,
|
||
mediaId: '',
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `novelId` | `String` | 是 | 小说 ID |
|
||
| `novelTitle` | `String` | 是 | 小说标题 |
|
||
| `novelTypeId` | `String` | 是 | 分类 ID |
|
||
| `novelTypeName` | `String` | 是 | 分类名称 |
|
||
| `novelTagKey` | `String` | 是 | 标签 KEY,多个用英文逗号分隔 |
|
||
| `novelTagName` | `String` | 是 | 标签名称,多个用英文逗号分隔 |
|
||
| `readProgress` | `int` | 是 | 阅读进度百分比(0-100) |
|
||
| `pageNo` | `int` | 是 | 当前阅读第几页(从 1 开始) |
|
||
| `novelBehavior` | `ReadBehaviorEnum` | 是 | 阅读行为,见 [ReadBehaviorEnum](#readbehaviorenum) |
|
||
| `recommendTraceId` | `String` | 否 | 推荐引擎 trace ID,未接推荐引擎传 `''`,默认 `''` |
|
||
| `mediaId` | `String` | 否 | 媒体资源 ID,未使用老司机库资源时传 `''` |
|
||
|
||
---
|
||
|
||
### 6.4 漫画阅读事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(ComicEvent(
|
||
comicId: 'comic_001',
|
||
comicTitle: '火影忍者',
|
||
comicTypeId: 'battle',
|
||
comicTypeName: '热血',
|
||
comicTagKey: 'hot,classic',
|
||
comicTagName: '热门,经典',
|
||
readProgress: 60,
|
||
pageNo: 5,
|
||
comicBehavior: ReadBehaviorEnum.PAGE_NEXT,
|
||
));
|
||
```
|
||
|
||
字段与 `NovelEvent` 对称,将 `novel*` 前缀替换为 `comic*`,`novelBehavior` 替换为 `comicBehavior`。`recommendTraceId` 字段同样支持,默认 `''`。
|
||
|
||
---
|
||
|
||
### 6.5 广告展示事件【客户端】(带自动去重)
|
||
|
||
> SDK 会对 `adId` 进行 session 级别去重:同一个 `adId` 在当前 session 内只会上报一次。
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(AdImpressionEvent(
|
||
pageKey: 'home',
|
||
pageName: '首页',
|
||
adSlotKey: 'home_banner_1',
|
||
adSlotName: '首页顶部Banner',
|
||
adId: 'ad_001,ad_002,ad_003',
|
||
creativeId: 'cr_001,cr_002,cr_003',
|
||
adType: 'banner', // 传广告类型字符串,不传默认空字符串
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `pageKey` | `String` | 是 | 页面标识 |
|
||
| `pageName` | `String` | 是 | 页面名称 |
|
||
| `adSlotKey` | `String` | 是 | 广告位标识 |
|
||
| `adSlotName` | `String` | 是 | 广告位名称 |
|
||
| `adId` | `String` | 是 | 广告 ID,多个用英文逗号分隔 |
|
||
| `creativeId` | `String` | 否 | 素材 ID,多个用英文逗号分隔,与 `adId` 一一对应,默认 `''` |
|
||
| `adType` | `String` | 否 | 广告类型字符串,如 `'banner'`、`'feed'`、`'splash'`,默认 `''` |
|
||
|
||
**去重逻辑**:
|
||
- 进入 TabA 展示 20 条广告 → 上报 20 条
|
||
- 切换到 TabB 展示 20 条广告 → 上报 20 条
|
||
- 返回 TabA 上滑新加载 10 条广告 → 上报新的 10 条
|
||
- 再次下滑看到之前已展示的广告 → 全部被过滤
|
||
|
||
---
|
||
|
||
### 6.6 广告点击事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(AdClickEvent(
|
||
pageKey: 'home',
|
||
pageName: '首页',
|
||
adSlotKey: 'home_banner_1',
|
||
adSlotName: '首页顶部Banner',
|
||
adId: 'ad_001',
|
||
creativeId: 'cr_001',
|
||
adType: 'banner',
|
||
));
|
||
```
|
||
|
||
字段与 `AdImpressionEvent` 相同,`adId` 为单个广告 ID。
|
||
|
||
---
|
||
|
||
### 6.7 APP 广告行为事件【客户端】(弹窗/Banner 行为)
|
||
|
||
> 用于上报 APP 内置广告(弹窗、Banner 等)的展示、点击、关闭行为。
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(AdvertisingEvent(
|
||
eventType: BannerEventEnum.CLICK.label,
|
||
advertisingKey: 'home_popup',
|
||
advertisingName: '首页弹窗',
|
||
advertisingId: 'adv_001',
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `eventType` | `String` | 行为类型:`click`/`close`/`show` |
|
||
| `advertisingKey` | `String` | 广告标识 |
|
||
| `advertisingName` | `String` | 广告名称 |
|
||
| `advertisingId` | `String` | 广告 ID |
|
||
|
||
---
|
||
|
||
### 6.8 关键词搜索事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(KeywordSearchEvent(
|
||
keyword: '火影忍者',
|
||
searchResultCount: 42,
|
||
searchTraceId: '', // 未接搜索引擎时传空字符串
|
||
searchId: '', // 未接搜索引擎时传空字符串
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `keyword` | `String` | 是 | 搜索关键词 |
|
||
| `searchResultCount` | `int` | 是 | 搜索结果数量 |
|
||
| `searchTraceId` | `String` | 否 | 搜索引擎 trace ID,未接搜索引擎时传 `''` |
|
||
| `searchId` | `String` | 否 | 搜索引擎 search ID,未接搜索引擎时传 `''` |
|
||
|
||
---
|
||
|
||
### 6.9 关键词点击事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(KeywordClickEvent(
|
||
keyword: '火影忍者',
|
||
clickItemId: 'vid_001',
|
||
clickItemType: ClickItemTypeEnum.VIDEO,
|
||
clickPosition: 1,
|
||
searchTraceId: '', // 未接搜索引擎时传空字符串
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `keyword` | `String` | 是 | 搜索关键词 |
|
||
| `clickItemId` | `String` | 是 | 点击项目 ID(视频ID / 小说ID 等) |
|
||
| `clickItemType` | `ClickItemTypeEnum` | 是 | 点击项目类型:`VIDEO` / `NOVEL` / `COMIC` |
|
||
| `clickPosition` | `int` | 是 | 点击位置(搜索结果中的排序位置,从 1 开始) |
|
||
| `searchTraceId` | `String` | 否 | 搜索引擎 trace ID,未接搜索引擎时传 `''` |
|
||
|
||
---
|
||
|
||
### 6.10 推荐列表展示事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(RecommendListViewEvent(
|
||
pageKey: 'home',
|
||
pageName: '首页',
|
||
recommendContentType: RecommendContentTypeEnum.video,
|
||
recommendTraceId: 'rec_trace_abc123',
|
||
recommendId: 'vid_001,vid_002,vid_003',
|
||
recommendTraceInfo: '{"source":"server"}', // 未接推荐引擎传 ''
|
||
clientVersion: '1.2.3', // 未接推荐引擎传 ''
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `pageKey` | `String` | 是 | 页面标识 |
|
||
| `pageName` | `String` | 是 | 页面名称 |
|
||
| `recommendContentType` | `RecommendContentTypeEnum` | 是 | 推荐内容类型,见 [RecommendContentTypeEnum](#recommendcontenttypeenum) |
|
||
| `recommendTraceId` | `String` | 是 | 推荐引擎 trace ID |
|
||
| `recommendId` | `String` | 是 | 可视窗口内的内容 ID,多个用英文逗号分隔 |
|
||
| `recommendTraceInfo` | `String` | 否 | 推荐引擎 trace info,未接推荐引擎传 `''`,默认 `''` |
|
||
| `clientVersion` | `String` | 否 | 客户端版本号,如 `'1.2.3'`,默认 `''` |
|
||
|
||
---
|
||
|
||
### 6.11 推荐列表点击事件【客户端】
|
||
|
||
```dart
|
||
AnalyticsSdk.instance.track(RecommendListClickEvent(
|
||
pageKey: 'home',
|
||
pageName: '首页',
|
||
recommendContentType: RecommendContentTypeEnum.video,
|
||
recommendTraceId: 'rec_trace_abc123',
|
||
recommendId: 'vid_001',
|
||
recommendTraceInfo: '{"source":"server"}', // 未接推荐引擎传 ''
|
||
clientVersion: '1.2.3', // 未接推荐引擎传 ''
|
||
));
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `pageKey` | `String` | 是 | 页面标识 |
|
||
| `pageName` | `String` | 是 | 页面名称 |
|
||
| `recommendContentType` | `RecommendContentTypeEnum` | 是 | 推荐内容类型,见 [RecommendContentTypeEnum](#recommendcontenttypeenum) |
|
||
| `recommendTraceId` | `String` | 是 | 推荐引擎 trace ID |
|
||
| `recommendId` | `String` | 是 | 被点击内容的 ID |
|
||
| `recommendTraceInfo` | `String` | 否 | 推荐引擎 trace info,未接推荐引擎传 `''`,默认 `''` |
|
||
| `clientVersion` | `String` | 否 | 客户端版本号,如 `'1.2.3'`,默认 `''` |
|
||
|
||
---
|
||
|
||
### 6.12 强制立即上报
|
||
|
||
```dart
|
||
await AnalyticsSdk.instance.flush();
|
||
```
|
||
|
||
### 6.13 释放资源
|
||
|
||
```dart
|
||
await AnalyticsSdk.instance.dispose();
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 枚举参考
|
||
|
||
### DeviceEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `DeviceEnum.android` | `Android` | Android 设备 |
|
||
| `DeviceEnum.iOS` | `iOS` | iOS 设备 |
|
||
| `DeviceEnum.pc` | `PC` | PC 设备 |
|
||
|
||
### UserTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `UserTypeEnum.normal` | `normal` | 普通用户 |
|
||
| `UserTypeEnum.vip` | `vip` | VIP 用户 |
|
||
|
||
### AuthTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `AuthTypeEnum.phone` | `phone` | 手机号 |
|
||
| `AuthTypeEnum.deviceid` | `deviceid` | 设备 ID |
|
||
| `AuthTypeEnum.email` | `email` | 邮箱 |
|
||
| `AuthTypeEnum.username` | `username` | 账号 |
|
||
|
||
### AdTypeEnum
|
||
|
||
> **已废弃**:`adType` 字段已改为普通 `String`,直接传字符串即可,如 `'banner'`、`'feed'`、`'splash'`。无需 import 枚举类。
|
||
|
||
### OrderTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `OrderTypeEnum.coinPurchase` | `coin_purchase` | 金币购买 |
|
||
| `OrderTypeEnum.vipSubscription` | `vip_subscription` | VIP 订阅 |
|
||
|
||
### CurrencyEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `CurrencyEnum.cny` | `CNY` | 人民币 |
|
||
| `CurrencyEnum.usd` | `USD` | 美元 |
|
||
|
||
### PayTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `PayTypeEnum.wechat` | `wechat` | 微信支付 |
|
||
| `PayTypeEnum.alipay` | `alipay` | 支付宝 |
|
||
| `PayTypeEnum.bankCard` | `bank_card` | 银行卡 |
|
||
| `PayTypeEnum.applePay` | `apple_pay` | Apple Pay |
|
||
|
||
### ConsumeReasonEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `ConsumeReasonEnum.videoUnlock` | `video_unlock` | 视频解锁 |
|
||
| `ConsumeReasonEnum.giftSend` | `gift_send` | 礼物赠送 |
|
||
| `ConsumeReasonEnum.contentPurchase` | `content_purchase` | 内容购买 |
|
||
|
||
### VideoEventEnum
|
||
|
||
| 枚举值 | key | 说明 |
|
||
|--------|-----|------|
|
||
| `VideoEventEnum.VIDEO_VIEW` | `video_view` | 展示 |
|
||
| `VideoEventEnum.VIDEO_PLAY` | `video_play` | 播放 |
|
||
| `VideoEventEnum.VIDEO_PAUSE` | `video_pause` | 暂停 |
|
||
| `VideoEventEnum.VIDEO_SHARE` | `video_share` | 分享 |
|
||
| `VideoEventEnum.VIDEO_COMPLETE` | `video_complete` | 播放完成 |
|
||
| `VideoEventEnum.VIDEO_FORWARD` | `video_forward` | 快进 |
|
||
| `VideoEventEnum.VIDEO_REWIND` | `video_rewind` | 快退 |
|
||
|
||
### VideoContentTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `VideoContentTypeEnum.video` | `video` | 长视频 |
|
||
| `VideoContentTypeEnum.shortVideo` | `short_video` | 短视频 |
|
||
|
||
### ReadBehaviorEnum
|
||
|
||
| 枚举值 | key | 说明 |
|
||
|--------|-----|------|
|
||
| `ReadBehaviorEnum.VIEW` | `view` | 展示 |
|
||
| `ReadBehaviorEnum.PAGE_NEXT` | `page_next` | 下一页 |
|
||
| `ReadBehaviorEnum.PAGE_PREV` | `page_prev` | 上一页 |
|
||
| `ReadBehaviorEnum.COMPLETE` | `complete` | 读完 |
|
||
|
||
### ClickItemTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `ClickItemTypeEnum.VIDEO` | `video` | 视频 |
|
||
| `ClickItemTypeEnum.NOVEL` | `novel` | 小说 |
|
||
| `ClickItemTypeEnum.COMIC` | `comic` | 漫画 |
|
||
|
||
### RecommendContentTypeEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `RecommendContentTypeEnum.video` | `video` | 视频 |
|
||
| `RecommendContentTypeEnum.novel` | `novel` | 小说 |
|
||
| `RecommendContentTypeEnum.comic` | `comic` | 漫画 |
|
||
|
||
### BannerEventEnum
|
||
|
||
| 枚举值 | label | 说明 |
|
||
|--------|-------|------|
|
||
| `BannerEventEnum.CLICK` | `click` | 点击 |
|
||
| `BannerEventEnum.CLOSE` | `close` | 关闭 |
|
||
| `BannerEventEnum.SHOW` | `show` | 展示 |
|
||
|
||
---
|
||
|
||
## 8. SdkConfig 全局参数
|
||
|
||
在 `AnalyticsSdk.instance.init()` **之前**调用 `SdkConfig.configure(...)` 覆盖默认值:
|
||
|
||
```dart
|
||
SdkConfig.configure(
|
||
uploadInterval: const Duration(seconds: 5),
|
||
autoUploadThreshold: 10,
|
||
maxBatchSize: 200,
|
||
connectionTimeout: const Duration(seconds: 15),
|
||
);
|
||
```
|
||
|
||
| 参数 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `maxQueueSize` | `5000` | 内存队列最大容量,超过后新事件丢弃 |
|
||
| `autoUploadThreshold` | `10` | 队列达到此数量时立即触发上报 |
|
||
| `maxBatchSize` | `500` | 单次上报最大事件数 |
|
||
| `uploadInterval` | `Duration(seconds: 5)` | 定时上报间隔 |
|
||
| `clickThrottleDuration` | `Duration(milliseconds: 300)` | 点击事件节流时间 |
|
||
| `connectionTimeout` | `Duration(seconds: 10)` | HTTP 连接超时 |
|
||
| `readTimeout` | `Duration(seconds: 30)` | HTTP 读取超时 |
|
||
| `speedTestTimeout` | `Duration(seconds: 5)` | 域名测速超时 |
|
||
| `maxCacheLines` | `2000` | 缓存文件最大行数 |
|
||
| `maxCacheBytes` | `2 * 1024 * 1024` (2MB) | 缓存文件最大字节数 |
|
||
| `maxSingleEventSize` | `300 * 1024` (300KB) | 单条事件最大字节数,超过丢弃 |
|
||
| `maxAdImpressionCapacity` | `10000` | 广告去重集合最大容量(FIFO 淘汰) |
|
||
|
||
恢复所有默认值:
|
||
|
||
```dart
|
||
SdkConfig.reset();
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 上报数据格式
|
||
|
||
### 9.1 公共字段
|
||
|
||
每个事件上报时,顶层 JSON 自动携带以下公共字段:
|
||
|
||
| 字段名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| `event` | `String` | 事件类型,如 `video_event`、`ad_impression` |
|
||
| `channel` | `String` | 渠道标识 |
|
||
| `app_id` | `String` | 应用 ID |
|
||
| `uid` | `String` | 用户 ID |
|
||
| `sid` | `String` | 会话 ID,自动管理 |
|
||
| `client_ts` | `int` | 事件创建时间戳(10位,秒) |
|
||
| `device` | `String` | 设备类型(自动采集) |
|
||
| `device_id` | `String` | 设备唯一标识 |
|
||
| `device_brand` | `String` | 设备品牌(自动采集) |
|
||
| `device_model` | `String` | 设备型号(自动采集) |
|
||
| `user_agent` | `String` | 客户端标识(自动采集) |
|
||
| `system_name` | `String` | 操作系统名称(自动采集) |
|
||
| `system_version` | `String` | 操作系统版本(自动采集) |
|
||
| `device_fingerprint` | `String` | 设备指纹(自动生成) |
|
||
| `fp_version` | `String` | 指纹版本号 |
|
||
| `event_id` | `String` | 事件去重 ID |
|
||
| `payload` | `Object` | 事件业务字段 |
|
||
|
||
### 9.2 上报数据格式
|
||
|
||
```json
|
||
{
|
||
"event": "video_event",
|
||
"channel": "google_play",
|
||
"app_id": "your_app_id",
|
||
"uid": "12345",
|
||
"sid": "sess_abc123",
|
||
"client_ts": 1704067200,
|
||
"device": "Android",
|
||
"device_id": "xxxx",
|
||
"device_brand": "Samsung",
|
||
"device_model": "SM-G991B",
|
||
"user_agent": "...",
|
||
"system_name": "Android",
|
||
"system_version": "14.0",
|
||
"device_fingerprint": "sha256-hex-xxxx",
|
||
"fp_version": "1",
|
||
"event_id": "md5-hash-for-dedup",
|
||
"payload": {
|
||
"event": "video_event",
|
||
"video_id": "vid_001",
|
||
...
|
||
}
|
||
}
|
||
```
|
||
|
||
### 9.3 字段校验规则
|
||
|
||
SDK 在事件入队前会自动校验并修正字段。**关键字段**不合规时整个事件被静默丢弃;**可纠正字段**超限时自动截断或补全,不影响上报。
|
||
|
||
关键字段包括:`event`、`event_id`、`app_id`、`sid`、`client_ts`、`device`(提供了无法识别的值时)、`device_id`、`payload`(类型错误时),以及 payload 中的核心业务 ID 字段(`video_id` / `novel_id` / `comic_id` / `click_item_id`)。
|
||
|
||
> **通用类型处理规则**
|
||
> - 所有字符串字段在校验前均会自动 `trim()`(去除首尾空格)。
|
||
> - 非字符串类型(如 `int`、`bool`、`null` 等)传入字符串字段时视为类型错误,等同于未传该字段(关键字段 → 丢弃事件;可选字段 → 移除该字段)。
|
||
> - 数字字段(百分比 / 位置 / 数量 / 时间戳类)支持 `int`、`double` 以及纯数字字符串三种类型,均可正常解析。
|
||
|
||
#### 公共字段
|
||
|
||
| 字段 | 规则 | 不合规处理 |
|
||
|------|------|-----------|
|
||
| `event` | 小写字母/数字/下划线,最大 64 字符 | 丢弃事件 |
|
||
| `event_id` | 字母/数字,1~32 字符(SDK 自动生成) | 丢弃事件 |
|
||
| `app_id` | 非空,最大 64 字符 | 空时丢弃事件;超长截断 |
|
||
| `channel` | 可选,最大 128 字符 | 超长截断 |
|
||
| `uid` | 可选,最大 128 字符 | 超长截断 |
|
||
| `sid` | 自动生成;为空时 SDK 自动填充会话 ID | 生成失败则丢弃事件 |
|
||
| `client_ts` | 整数,范围 946684800 ~ 4102444800(2000 ~ 2100 年) | 丢弃事件 |
|
||
| `device` | 仅 `Android` / `iOS` / `PC`,大小写自动纠正 | 丢弃事件 |
|
||
| `device_id` | 非空,最大 128 字符 | 空时丢弃事件;超长截断 |
|
||
| `device_fingerprint` | 可选,最大 128 字符 | 超长截断 |
|
||
| `fp_version` | 若指纹非空而版本为空,自动填充默认版本 | — |
|
||
|
||
#### payload 业务字段
|
||
|
||
| 类别 | 代表字段 | 规则 | 不合规处理 |
|
||
|------|---------|------|-----------|
|
||
| 字段名 | 所有 key | `^[A-Za-z0-9_]+$` | 移除该字段 |
|
||
| 关键 ID | `video_id` / `novel_id` / `comic_id` / `click_item_id` | 非空 | 丢弃事件 |
|
||
| 普通 ID | `ad_id` / `recommend_id` 等 | 非空 | 移除该字段 |
|
||
| 逗号分隔 | `ad_id` / `novel_tag_key` / `video_tag_name` 等 | 自动清理各段首尾空格、过滤空段;清洗后仍为空则移除该字段 | 移除该字段 |
|
||
| 可选字符串 | `creative_id` / `recommend_trace_id` 等 | 可为空 | 空字符串原样保留 |
|
||
| 名称字段 | `page_name` / `ad_name` / `video_name` 等 | 最大 128 字符 | 截断 |
|
||
| 标题字段 | `title` / `content_title` / `search_keyword` 等 | 最大 256 字符 | 截断 |
|
||
| 关键词字段 | `keywords` / `tags` / `description` | 最大 500 字符 | 截断 |
|
||
| 百分比字段 | `progress` / `completion_rate` / `view_ratio` | 整数,0 ~ 100 | 移除该字段 |
|
||
| 位置字段 | `position` / `rank` / `slot` / `index` | 整数,≥ 1 | 移除该字段 |
|
||
| 数字字段 | `duration` / `count` / `amount` 等 | 整数,≥ 0 | 移除该字段 |
|
||
| 时间戳字段 | `start_ts` / `end_ts` / `publish_ts` | 同 `client_ts` 范围 | 移除该字段 |
|
||
| 其他字段 | — | 字符串最大 500 字符;非字符串原样保留 | 截断 |
|
||
|
||
### 9.4 设备指纹(device_fingerprint / fp_version)
|
||
|
||
设备指纹由 SDK 在 `init()` 时自动生成,业务方**无需**手动传入。
|
||
|
||
#### 采集因子
|
||
|
||
| 平台 | 参与计算的因子 |
|
||
|------|--------------|
|
||
| Native(Android / iOS / 桌面) | `device_id`、`device`(类型)、`device_brand`、`device_model`、`system_name`、`system_version` |
|
||
| Web | `device`、`device_brand`、`device_model`、`system_name`、`system_version`、`user_agent`、屏幕分辨率(`WxH`)、浏览器语言、时区名称 |
|
||
|
||
#### 生成规则
|
||
|
||
1. 过滤所有空因子,将剩余因子用 `|` 拼接后做 **SHA-256**,取 64 位十六进制字符串作为指纹。
|
||
2. 若**所有因子均为空**(降级场景),改为生成随机 **UUID v4** 并持久化至本地(见缓存规则),后续从本地读取保证稳定性。
|
||
|
||
> `device_fingerprint` **不参与** `event_id` 的计算。
|
||
|
||
#### 缓存规则
|
||
|
||
- **内存缓存**:首次生成后缓存在内存中,同一应用生命周期内直接返回,不重复计算。
|
||
- **持久化缓存**(仅降级 UUID 路径):
|
||
- Native:写入 `applicationDocumentsDirectory/analytics_fingerprint.txt`;
|
||
- Web:写入 `localStorage['analytics_fp']`。
|
||
- 重新 `init()` 后若设备因子发生变化,可通过内部 `clearCache()` 重新计算(一般无需手动调用)。
|
||
|
||
#### 失败兜底
|
||
|
||
任何环节发生异常,均静默返回空字符串,事件照常上报;`device_fingerprint` 为空时,`fp_version` 字段亦不写入(无指纹则无需版本号)。
|
||
|
||
#### fp_version 说明
|
||
|
||
`fp_version` 标识当前指纹生成规则的版本(当前为 `"1"`),供服务端区分不同规则生成的指纹,支持跨版本数据对比分析。生成规则(因子组合或哈希算法)变更时版本号递增。
|
||
|
||
---
|
||
|
||
## 10. 调试工具
|
||
|
||
### 10.1 AnalyticsDebugBanner(悬浮调试条)
|
||
|
||
在 `init()` 时传 `enableDebugBanner: true`,然后将 `AnalyticsDebugBanner()` 放入 widget 树顶部(通常是页面 Column 的第一个子节点或 Stack 的顶层):
|
||
|
||
```dart
|
||
await AnalyticsSdk.instance.init(
|
||
// ...
|
||
enableDebugBanner: true,
|
||
);
|
||
|
||
class MyApp extends StatelessWidget {
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
home: Scaffold(
|
||
body: Column(
|
||
children: [
|
||
const AnalyticsDebugBanner(), // 无需任何参数
|
||
Expanded(child: YourContent()),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
调试条显示 7 个步骤:①域名配置 ②SDK初始化 ③域名列表 ④域名测速 ⑤上报地址 ⑥待上报队列 ⑦最近上报。release 包自动隐藏,无性能影响。
|
||
|
||
### 10.2 getDebugSteps / getDebugInfo
|
||
|
||
```dart
|
||
final steps = AnalyticsSdk.instance.getDebugSteps();
|
||
for (final step in steps) {
|
||
print('${step['name']}: ${step['status']} - ${step['detail']}');
|
||
}
|
||
|
||
final info = AnalyticsSdk.instance.getDebugInfo();
|
||
print(info['inited']);
|
||
print(info['queueLength']);
|
||
print(info['reportUrl']);
|
||
```
|
||
|
||
### 10.3 validateEncryptedConfig
|
||
|
||
```dart
|
||
final result = AnalyticsSdk.instance.validateEncryptedConfig(encryptedString);
|
||
if (result['success'] == true) {
|
||
print('域名数量: ${result['domainCount']}');
|
||
print('事件类型数量: ${result['eventCount']}');
|
||
} else {
|
||
print('验证失败: ${result['error']}');
|
||
}
|
||
```
|
||
|
||
仅 debug 包可用,release 包返回错误。
|
||
|
||
### 10.4 Logger
|
||
|
||
```dart
|
||
import 'package:analytics_sdk/utils/logger.dart';
|
||
|
||
Logger.enabled = false; // 关闭所有日志
|
||
```
|
||
|
||
---
|
||
|
||
## 11. 高级用法
|
||
|
||
### 11.1 延迟传入加密配置
|
||
|
||
```dart
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
await AnalyticsSdk.instance.init(
|
||
appId: 'your_app_id',
|
||
encryptedConfig: null, // SDK 尝试读取磁盘缓存域名
|
||
deviceId: await getDeviceId(),
|
||
);
|
||
|
||
runApp(const MyApp());
|
||
|
||
// 异步获取,不阻塞 UI 启动
|
||
_fetchAndApplyConfig();
|
||
}
|
||
|
||
Future<void> _fetchAndApplyConfig() async {
|
||
try {
|
||
final config = await yourApiClient.fetchEncryptedConfig();
|
||
await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: config);
|
||
} catch (_) {}
|
||
}
|
||
```
|
||
|
||
### 11.2 获取公共参数(getParams)
|
||
|
||
`getParams()` 可获取 SDK 当前全部公共参数,适合服务端拼装事件或埋点调试:
|
||
|
||
```dart
|
||
// 获取全部参数
|
||
final all = AnalyticsSdk.getParams();
|
||
|
||
// 只取指定字段
|
||
final partial = AnalyticsSdk.getParams(['app_id', 'uid', 'device_id', 'device_fingerprint']);
|
||
```
|
||
|
||
返回 `Map<String, dynamic>`,包含:`app_id`、`channel`、`uid`、`user_type`、`sid`、`device`、`device_id`、`device_brand`、`device_model`、`user_agent`、`system_name`、`system_version`、`device_fingerprint`、`fp_version`。
|
||
|
||
如果 key 不存在,该 key 不会出现在返回 Map 中(静默忽略)。
|
||
|
||
### 11.3 服务端复用设备公共字段
|
||
|
||
```dart
|
||
final deviceFields = AnalyticsSdk.getDeviceCommonFields();
|
||
// 包含:device, device_id, device_brand, device_model,
|
||
// user_agent, system_name, system_version, device_fingerprint
|
||
```
|
||
|
||
---
|
||
|
||
## 12. 完整集成示例
|
||
|
||
```dart
|
||
import 'package:flutter/material.dart';
|
||
import 'package:analytics_sdk/analytics_sdk.dart';
|
||
import 'package:analytics_sdk/config/sdk_config.dart';
|
||
import 'package:analytics_sdk/enum/user_type_enum.dart';
|
||
import 'package:analytics_sdk/manager/page_name_manager.dart';
|
||
import 'package:analytics_sdk/widget/global_click_wrapper.dart';
|
||
|
||
void main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
SdkConfig.configure(
|
||
uploadInterval: const Duration(seconds: 5),
|
||
autoUploadThreshold: 10,
|
||
);
|
||
|
||
PageNameMapper.addMappings({
|
||
'home': '首页',
|
||
'video_detail': '视频详情页',
|
||
'user_center': '个人中心',
|
||
'search': '搜索页',
|
||
});
|
||
|
||
await AnalyticsSdk.instance.init(
|
||
appId: 'your_app_id',
|
||
encryptedConfig: 'YOUR_BASE64_ENCRYPTED_CONFIG',
|
||
deviceId: 'device_unique_id',
|
||
channel: 'google_play',
|
||
uid: '',
|
||
enableDebugBanner: true,
|
||
);
|
||
|
||
runApp(const MyApp());
|
||
}
|
||
|
||
class MyApp extends StatelessWidget {
|
||
const MyApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return GlobalClickWrapper(
|
||
child: MaterialApp(
|
||
navigatorObservers: [
|
||
AnalyticsSdk.instance.pageObserver,
|
||
],
|
||
initialRoute: '/',
|
||
routes: {
|
||
'/': (context) => const HomePage(),
|
||
'/video_detail': (context) => const VideoDetailPage(),
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ——— 登录场景 ———
|
||
void onLoginSuccess(String userId, bool isVip) {
|
||
AnalyticsSdk.setUserIdAndType(
|
||
userId: userId,
|
||
userTypeEnum: isVip ? UserTypeEnum.vip : UserTypeEnum.normal,
|
||
);
|
||
}
|
||
|
||
// ——— 视频播放场景 ———
|
||
void onVideoPlay(String videoId, String title) {
|
||
AnalyticsSdk.instance.track(VideoEvent(
|
||
videoId: videoId,
|
||
videoTitle: title,
|
||
videoTypeId: 'action',
|
||
videoTypeName: '动作片',
|
||
videoTagKey: 'hot',
|
||
videoTagName: '热门',
|
||
videoDuration: 7200,
|
||
playDuration: 0,
|
||
playProgress: 0,
|
||
videoBehavior: VideoEventEnum.VIDEO_PLAY,
|
||
));
|
||
}
|
||
|
||
// ——— 广告展示场景 ———
|
||
void onAdsRendered(List<String> adIds) {
|
||
AnalyticsSdk.instance.track(AdImpressionEvent(
|
||
pageKey: 'home',
|
||
pageName: '首页',
|
||
adSlotKey: 'home_feed_ad',
|
||
adSlotName: '首页信息流广告',
|
||
adId: adIds.join(','),
|
||
adType: 'feed',
|
||
));
|
||
}
|
||
|
||
// ——— 应用退出场景 ———
|
||
Future<void> onAppExit() async {
|
||
await AnalyticsSdk.instance.flush();
|
||
await AnalyticsSdk.instance.dispose();
|
||
}
|
||
```
|