Files
hjsq_android/analytics_sdk/README.md
T
2026-10-08 11:37:25 +07:00

1159 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> **版本说明:** 本分支(`dev`)适用于 **Flutter 3.0.5 及以上**(Dart >=2.17)。
> 如使用 Flutter 1.22.5,请切换至 `compat/flutter-1.22` 分支。
# Analytics SDK
一个 Flutter 埋点 SDK,用于采集 App 内页面、点击、广告、视频、小说、漫画、搜索、推荐等行为,并提供批量上报、本地持久化、域名测速和事件类型动态配置能力。
## 目录
- [功能特性](#功能特性)
- [安装](#安装)
- [快速开始](#快速开始)
- [核心 API](#核心-api)
- [域名与事件类型管理](#域名与事件类型管理)
- [SDK 配置(可选)](#sdk-配置可选)
- [更多文档](#更多文档)
- [手动上报事件](#手动上报事件)
- [用户类型管理](#用户类型管理)
- [页面名称映射](#页面名称映射)
- [事件数据结构](#事件数据结构)
- [上报机制](#上报机制)
- [事件去重](#事件去重)
- [完整示例](#完整示例)
- [枚举类型参考](#枚举类型参考)
- [注意事项](#注意事项)
- [调试](#调试)
- [常见问题 FAQ](#常见问题-faq)
- [依赖项](#依赖项)
- [环境要求](#环境要求)
## 功能特性
- **多事件类型**:页面浏览、页面点击、导航、广告展示/点击、视频/小说/漫画行为、关键词搜索、推荐等事件
- **自动埋点**:页面生命周期(曝光/停留)、全局点击(默认开启)
- **批量上报**:队列聚合,默认每 5 秒或队列达 10 条时批量上报
- **本地持久化**:事件写入本地 JSONL 文件,重启后自动恢复(默认最多 2000 行)
- **智能重试**:指数退避重试机制,初始 2s,最大 60s,最多 5 级
- **域名测速**:从 `encryptedConfig` 解析域名列表,并发测速选最快可用节点
- **事件类型动态开关**:通过 `encryptedConfig` 下发白名单,SDK 侧统一过滤
- **会话管理**:按"冷启动 / 前后台 30 分钟 / 行为 30 分钟"规则生成 `sid`
- **用户管理**:支持用户 ID 与用户类型动态更新
- **广告展示去重**:session 级别按 adId 自动去重,防止重复上报
- **设备信息自动采集**:通过 `device_info_plus` 自动获取品牌、型号、系统名称与版本,无需外部传入
- **设备指纹**:基于设备因子生成 SHA-256 指纹;因子全空时降级为 UUID v4 持久化兜底
- **公共参数暴露**:通过 `getParams()` 获取全部或指定公共埋点参数,供服务端复用
## 安装
在宿主工程的 `pubspec.yaml` 中添加依赖(示例):
```yaml
dependencies:
analytics_sdk:
path: ../analytics_sdk # 本地导入:根据实际目录调整相对路径
```
然后执行:
```bash
flutter pub get
```
## 快速开始
### 1. 初始化 SDK
```dart
import 'package:flutter/widgets.dart';
import 'package:analytics_sdk/analytics_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 从你的服务端获取加密配置(由业务方自行实现网络请求)
final encryptedConfig = await yourApiClient.fetchEncryptedConfig();
await AnalyticsSdk.instance.init(
appId: 'your_app_id',
encryptedConfig: encryptedConfig, // 加密配置,解密后包含域名列表和事件白名单
channel: 'official',
uid: 'user_123', // 未登录可传 null 或空字符串
deviceId: 'device_unique_id', // 设备唯一标识
);
runApp(const MyApp());
}
```
> 设备品牌、型号、系统名称和版本由 SDK 内部通过 `device_info_plus` 自动采集,无需外部传入。
### 2. 支持「先 init,后补 encryptedConfig」的场景
如果你需要先初始化 SDK,再从自己配置接口拿到加密串,可以这样用:
```dart
// 1)先初始化(此时还没有 encryptedConfig,事件会正常入队和落盘,但无法实际上报)
await AnalyticsSdk.instance.init(
appId: 'your_app_id',
encryptedConfig: null, // 先传 null,SDK 会尝试读取本地缓存域名
deviceId: 'device_unique_id',
);
// 2)等拿到配置后再传入 encryptedConfig
final encryptedConfig = await yourApiClient.fetchEncryptedConfig();
await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: encryptedConfig);
```
`refreshDomainConfig()` 会:
- 解密 `encryptedConfig`,解析最新域名列表并并发测速,选出最快可用域名;
- 更新内部上报 URL,后续定时上报和 `flush()` 会把之前队列里的事件一并发出;
- 更新事件类型白名单配置。
### 3. 页面生命周期 & 全局点击埋点
```dart
import 'package:analytics_sdk/analytics_sdk.dart';
import 'package:analytics_sdk/widget/global_click_wrapper.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [
AnalyticsSdk.instance.pageObserver, // 页面自动埋点
],
builder: (context, child) {
return GlobalClickWrapper( // 全局点击自动抓取
child: child!,
);
},
home: const HomePage(),
);
}
}
```
## 核心 API
### AnalyticsSdk.init
```dart
Future<void> init({
required String appId,
required String? encryptedConfig,
String? channel,
String? uid,
required String deviceId,
bool enableDebugBanner = false,
})
```
建议在 `main()` 中、`runApp()` 之前调用。内部流程:
- 通过 `device_info_plus` 自动采集设备品牌、型号、系统名称、版本等信息;
- 配置公共字段(appId / channel / uid / device 等);
- 初始化 SessionManager、AppLifecycleObserver、设备指纹;
- 解密 `encryptedConfig`,解析域名列表并发测速选最快节点;
- 解析事件类型白名单;
- 如 `encryptedConfig` 为 null / 解密失败,尝试读取本地缓存域名和事件类型配置;
- 初始化事件队列、本地缓存和定时上报定时器。
**`enableDebugBanner`**:仅在非 release 包下生效,配合 `AnalyticsDebugBanner` 显示调试悬浮条。
### AnalyticsSdk.getParams
```dart
static Map<String, dynamic> getParams([List<String>? keys])
```
获取 SDK 当前全部公共参数(或按 key 过滤子集),可用于服务端事件拼装或调试:
```dart
// 获取全部公共参数
final all = AnalyticsSdk.getParams();
// { 'app_id': '...', 'channel': '...', 'uid': '...', 'user_type': '...', 'sid': '...', 'device': '...', ... }
// 只取指定参数
final partial = AnalyticsSdk.getParams(['app_id', 'uid', 'device_id']);
```
返回字段包括:`app_id`、`channel`、`uid`、`user_type`、`sid`、`device`、`device_id`、`device_brand`、`device_model`、`user_agent`、`system_name`、`system_version`、`device_fingerprint`、`fp_version`。
### AnalyticsSdk.refreshDomainConfig
```dart
Future<void> refreshDomainConfig({String? encryptedConfig})
```
使用场景:
- 先调用 `init()`,再从服务端获取 `encryptedConfig`;
- 运行中需要更新域名列表或事件类型白名单。
```dart
// 获取到最新加密配置后刷新
final newConfig = await yourApiClient.fetchEncryptedConfig();
await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: newConfig);
```
> 传入 null 或空字符串时忽略本次刷新。本方法永远不会抛出异常。
### 事件上报相关
```dart
// 上报内置事件
void track(dynamic event)
// 立即触发上报
Future<void> flush()
// 页面生命周期观察者
NavigatorObserver get pageObserver
// 资源清理(可选)
Future<void> dispose()
```
`track()` 支持传入 SDK 内置的事件类(如 `VideoEvent`、`AdImpressionEvent` 等)或直接传 `Map<String, dynamic>`。
SDK 会自动:
- 做广告展示事件去重与过滤;
- 检查事件类型是否在白名单中;
- 校验必填字段;
- 检查单条事件大小(超过 300KB 丢弃);
- 将事件写入队列和本地缓存;
- 在队列长度达到阈值或定时器触发时批量上报。
### 用户与会话
```dart
// 设置用户 ID 和类型(登录 / 切换账号)
static void setUserIdAndType({String userId = "", UserTypeEnum? userTypeEnum})
// 只更新用户 ID(不改变用户类型)
static void setUid(String uid)
// 更新用户类型
void updateUserType(String newType)
// 登出(清空 uid 和 userType)
static void logoutUser()
// 更新渠道标识
static void setChannel(String channel)
```
`sid`(会话 ID)由 SessionManager 自动管理,无需手动干预。
### 获取设备公共字段(服务端复用)
```dart
static Map<String, String> getDeviceCommonFields()
```
返回字段:`device`、`device_id`、`device_brand`、`device_model`、`user_agent`、`system_name`、`system_version`、`device_fingerprint`。可将这些字段传递给服务端,由服务端上报事件时附加。
## 域名与事件类型管理
### encryptedConfig 格式
`encryptedConfig` 是宿主 App 从自己服务端获取的 **Base64 编码 AES-GCM 密文**,SDK 内部解密后格式为:
```json
{
"domainList": [
"https://api1.example.com",
"https://api2.example.com"
],
"eventList": [
"app_page_view",
"page_click",
"video_event",
"ad_impression"
]
}
```
- **`domainList`**:上报域名列表,SDK 启动时会并发测速(GET 请求),选择响应最快的域名上报。
- **`eventList`**:启用的事件类型白名单。**不在列表中的事件会被静默丢弃**。传空列表时,所有事件类型均允许上报(向后兼容)。
### 测速与降级策略
1. 解密 `encryptedConfig` 成功 → 用 `domainList` 并发测速,选最快域名;
2. `encryptedConfig` 为 null 或解密失败 → 尝试从磁盘缓存(`data_plus_domains_cache.json`)读取上次保存的域名列表;
3. 缓存也为空 → 上报 URL 保持未配置,事件缓存在本地,等 `refreshDomainConfig()` 成功后自动上报。
### 验证加密配置(调试包)
```dart
final result = AnalyticsSdk.instance.validateEncryptedConfig(encryptedString);
if (result['success'] == true) {
print('条目数量: ${result['count']}');
print('第一条预览: ${result['preview']}');
} else {
print('验证失败: ${result['error']}');
}
```
> 此方法仅在 debug 包可用,release 包直接返回 `{'success': false}`。
## SDK 配置(可选)
通过 `SdkConfig.configure()` 调整 SDK 参数,需在 `init()` **之前**调用:
```dart
import 'package:analytics_sdk/config/sdk_config.dart';
SdkConfig.configure(
maxQueueSize: 5000,
uploadInterval: const Duration(seconds: 10),
maxCacheLines: 2000,
connectionTimeout: const Duration(seconds: 15),
);
```
常用配置:
**队列与批量**
- `maxQueueSize`:事件队列最大容量(默认 `5000`)
- `autoUploadThreshold`:达到阈值时自动触发上报(默认 `10`)
- `maxBatchSize`:单次上报最大事件数(默认 `500`)
- `maxJsonSize`:单次请求 JSON 最大字节数(默认 `5MB`)
- `maxSingleEventSize`:单条事件最大字节数,超过则丢弃(默认 `300KB`)
**定时器**
- `uploadInterval`:定时上报间隔(默认 `5秒`)
- `clickThrottleDuration`:全局点击节流时间(默认 `300毫秒`)
- `speedTestTimeout`:域名测速超时(默认 `5秒`)
**网络超时**
- `connectionTimeout`:HTTP 连接超时(默认 `10秒`)
- `readTimeout`:HTTP 读取超时(默认 `30秒`)
**缓存相关**
- `cacheFileName`:事件缓存文件名(默认 `data_plus_events_cache.jsonl`)
- `maxCacheLines`:缓存文件最大行数(默认 `2000`)
- `maxCacheBytes`:缓存文件最大字节数(默认 `2MB`)
- `tombstoneCompactionThreshold`:tombstone 文件达到此条目数时触发主缓存压缩(默认 `200`)
- `tombstoneFileName`:tombstone 文件名(默认 `data_plus_events_tombstone.txt`)
**重试**
- `baseRetryDelay`:基础重试延迟(默认 `2秒`)
- `maxRetryDelay`:最大重试延迟(默认 `60秒`)
- `maxBackoffLevel`:最大退避级别(默认 `5`)
**广告去重**
- `maxAdImpressionCapacity`:广告去重集合最大容量,FIFO 淘汰(默认 `10000`)
恢复所有默认值:
```dart
SdkConfig.reset();
```
## 更多文档
本仓库根目录提供了更详细的开发文档:
- **SDK_USAGE_GUIDE.md**:完整使用指南,包含所有内置事件的数据结构与字段说明、会话规则、自动埋点细节、性能与容错机制说明等。
建议接入前先通读 `SDK_USAGE_GUIDE.md`,本 README 主要用于快速了解和集成。
---
## 手动上报事件
```dart
void track(dynamic event)
```
上报任意事件,传入 SDK 内置事件类即可。
### 1. App 安装事件(客户端)
> 首次安装启动时,读取剪贴板获取到 `traceId` 后上报。
```dart
AnalyticsSdk.instance.track(AppInstallEvent(
traceId: 'landing_page_trace_id_xxx', // 落地页点击生成的唯一 ID
));
```
### 2. 视频行为事件(客户端)
```dart
import 'package:analytics_sdk/enum/video_event_enum.dart';
AnalyticsSdk.instance.track(VideoEvent(
videoId: 'video_123',
videoTitle: '示例视频',
videoTypeId: 'type_1',
videoTypeName: '娱乐',
videoTagKey: 'tag_1,tag_2',
videoTagName: '热门,最新',
videoDuration: 300,
playDuration: 60,
playProgress: 20,
videoBehavior: VideoEventEnum.VIDEO_PLAY,
mediaId: '',
));
```
### 3. 小说阅读事件(客户端)
```dart
import 'package:analytics_sdk/enum/read_behavior_enum.dart';
AnalyticsSdk.instance.track(NovelEvent(
novelId: 'novel_123',
novelTitle: '示例小说',
novelTypeId: 'type_1',
novelTypeName: '玄幻',
novelTagKey: 'tag_1,tag_2',
novelTagName: '玄幻,穿越',
readProgress: 50,
pageNo: 10,
novelBehavior: ReadBehaviorEnum.PAGE_NEXT,
));
```
### 4. 漫画阅读事件(客户端)
```dart
AnalyticsSdk.instance.track(ComicEvent(
comicId: 'comic_123',
comicTitle: '示例漫画',
comicTypeId: 'type_1',
comicTypeName: '热血',
comicTagKey: 'tag_1,tag_2',
comicTagName: '热血,冒险',
readProgress: 30,
pageNo: 5,
comicBehavior: ReadBehaviorEnum.VIEW,
));
```
### 5. 广告展示事件(客户端,带自动去重)
> SDK 会对 `adId` 进行 session 级别去重:同一个 `adId` 在当前 session 内只会上报一次。
```dart
import 'package:analytics_sdk/enum/ad_type_enum.dart';
AnalyticsSdk.instance.track(AdImpressionEvent(
pageKey: PageLifecycleObserver.currentPageKey,
pageName: PageNameMapper.getPageName(PageLifecycleObserver.currentPageKey),
adSlotKey: 'home_banner_1',
adSlotName: '首页顶部Banner',
adId: 'ad_123,ad_456,ad_789', // 多个 adId 英文逗号分隔
creativeId: 'cr_001,cr_002,cr_003', // 与 adId 一一对应,可传 null
adType: AdTypeEnum.banner,
));
```
### 6. 广告点击事件(客户端)
```dart
AnalyticsSdk.instance.track(AdClickEvent(
pageKey: PageLifecycleObserver.currentPageKey,
pageName: PageNameMapper.getPageName(PageLifecycleObserver.currentPageKey),
adSlotKey: 'home_banner_1',
adSlotName: '首页顶部Banner',
adId: 'ad_123',
adType: AdTypeEnum.banner,
));
```
### 7. APP 广告行为事件(客户端)
> 用于上报 APP 内置活动弹窗、运营 Banner 等的展示、点击、关闭行为。
```dart
import 'package:analytics_sdk/enum/banner_event_enum.dart';
AnalyticsSdk.instance.track(AdvertisingEvent(
eventType: BannerEventEnum.CLICK.label, // 'click'
advertisingKey: 'home_banner',
advertisingName: '首页Banner',
advertisingId: 'ad_001',
));
```
### 8. 关键词搜索事件(客户端)
```dart
AnalyticsSdk.instance.track(KeywordSearchEvent(
keyword: '示例关键词',
searchResultCount: 100,
));
```
### 9. 关键词搜索结果点击事件(客户端)
```dart
import 'package:analytics_sdk/enum/click_item_type_enum.dart';
AnalyticsSdk.instance.track(KeywordClickEvent(
keyword: '示例关键词',
clickItemId: 'video_123',
clickItemType: ClickItemTypeEnum.VIDEO,
clickPosition: 1,
));
```
### 10. 推荐列表展示事件(客户端)
```dart
AnalyticsSdk.instance.track(RecommendListViewEvent(
pageKey: 'home',
pageName: '首页',
recommendContentType: RecommendContentTypeEnum.video,
recommendTraceId: 'rec_trace_abc123',
recommendId: 'vid_001,vid_002,vid_003',
));
```
### 11. 推荐列表点击事件(客户端)
```dart
AnalyticsSdk.instance.track(RecommendListClickEvent(
pageKey: 'home',
pageName: '首页',
recommendTraceId: 'rec_trace_abc123',
recommendId: 'vid_001',
));
```
### 强制立即上报
```dart
await AnalyticsSdk.instance.flush();
```
### 资源清理
建议在应用退出前调用,SDK 会先尝试上报剩余事件,再释放所有资源:
```dart
await AnalyticsSdk.instance.dispose();
```
---
## 用户类型管理
```dart
import 'package:analytics_sdk/enum/user_type_enum.dart';
// 用户登录时设置用户 ID 和类型
AnalyticsSdk.setUserIdAndType(
userId: 'user_123',
userTypeEnum: UserTypeEnum.vip,
);
// 只更新用户类型(升级会员等)
AnalyticsSdk.instance.updateUserType(UserTypeEnum.vip.label);
// 用户登出
AnalyticsSdk.logoutUser();
```
---
## 页面名称映射
SDK 使用路由 key(`RouteSettings.name`)作为 `pageKey`,建议在应用启动时统一注册映射:
```dart
import 'package:analytics_sdk/manager/page_name_manager.dart';
// 批量添加
PageNameMapper.addMappings({
'/': '首页',
'/video_detail': '视频详情页',
'/user_center': '个人中心',
'/search': '搜索页',
});
// 单条添加
PageNameMapper.addMapping('/settings', '设置页');
// 获取
String pageName = PageNameMapper.getPageName('/video_detail'); // '视频详情页'
```
### 动态获取当前页面信息
```dart
import 'package:analytics_sdk/observer/page_lifecycle_observer.dart';
import 'package:analytics_sdk/manager/page_name_manager.dart';
AnalyticsSdk.instance.track(AdImpressionEvent(
pageKey: PageLifecycleObserver.currentPageKey,
pageName: PageNameMapper.getPageName(PageLifecycleObserver.currentPageKey),
adSlotKey: 'home_banner_1',
adSlotName: '首页顶部Banner',
adId: 'ad_123',
adType: AdTypeEnum.banner,
));
```
---
## 事件数据结构
所有事件上报时都包含以下公共字段(顶层):
| 字段名 | 类型 | 说明 |
|--------|------|------|
| `event` | String | 事件类型,如 `video_event` |
| `channel` | String | 渠道标识 |
| `app_id` | String | 应用 ID |
| `uid` | String | 用户 ID |
| `sid` | String | 会话 ID(冷启动或后台超 30 分钟时重新生成) |
| `client_ts` | int | 事件对象**创建时**的时间戳(10位秒级) |
| `device` | String | 设备类型(SDK 自动检测) |
| `device_id` | String | 设备唯一标识(由 `init(deviceId)` 传入) |
| `device_brand` | String | 设备品牌(SDK 自动检测) |
| `device_model` | String | 设备型号(SDK 自动检测) |
| `user_agent` | String | 客户端 UA(SDK 自动检测) |
| `system_name` | String | 操作系统名称(SDK 自动检测) |
| `system_version` | String | 操作系统版本(SDK 自动检测) |
| `device_fingerprint` | String | 设备指纹(SHA-256;因子全空时降级为 UUID v4) |
| `fp_version` | String | 指纹生成规则版本号(当前为 `1`) |
| `event_id` | String | 事件去重 ID(双重 MD5) |
| `payload` | Object | 事件业务字段 |
> `device_fingerprint` 和 `fp_version` 均不参与 `event_id` 的 MD5 计算。
---
## 上报机制
### 批量上报地址
```
POST {fastestDomain}/api/eventTracking/batchReport.json?appId={appId}
Content-Type: application/json
Body: [ {...事件1}, {...事件2}, ... ]
```
### 触发条件
- 队列达到 10 条(`SdkConfig.autoUploadThreshold`)
- 定时器每 5 秒触发(`SdkConfig.uploadInterval`)
- 手动调用 `flush()`
### 本地持久化
- 事件自动保存到 `data_plus_events_cache.jsonl`(应用文档目录)
- 应用重启后自动从缓存恢复并上报;恢复时会过滤掉已通过 tombstone 标记为已上报的事件,以及当前事件类型白名单中未启用的事件类型
- 上报成功后,已上报的 `event_id` 追加写入 tombstone 文件(`data_plus_events_tombstone.txt`),避免每次上报都重写整个缓存文件;tombstone 条目达到 200 条(`SdkConfig.tombstoneCompactionThreshold`)时触发一次主缓存压缩合并
### 重试机制
失败后指数退避重试:`2s → 4s → 8s → 16s → 32s`,超过 60s 上限后不再翻倍,最多重试 5 级。
---
## 事件去重
### 通用事件去重
每个事件都有唯一的 `event_id`(基于所有字段的双重 MD5 计算),服务端可以使用 `event_id` 去重。
### 广告展示事件去重
`AdImpressionEvent` 在客户端进行额外去重:
- 基于 `adId` 去重,同一 `adId` 在当前 session 只上报一次
- 支持多个 `adId` 逗号分隔,只上报未曾上报的 ID
- 去重记录在内存中,应用重启后清空
- 容量上限默认 10000 条,超出时 FIFO 淘汰最旧记录
去重管理 API:
```dart
import 'package:analytics_sdk/manager/ad_impression_manager.dart';
// 检查广告 ID 是否已上报
bool isReported = AdImpressionManager.instance.isReported('ad_123');
// 获取多个 ID 中哪些未上报
List<String> unreported = AdImpressionManager.instance.getUnreportedAdIds('ad_123,ad_456');
// 清除已上报记录(测试用)
AdImpressionManager.instance.clear();
```
---
## 完整示例
```dart
import 'package:analytics_sdk/analytics_sdk.dart';
import 'package:analytics_sdk/config/sdk_config.dart';
import 'package:analytics_sdk/entity/video_event.dart';
import 'package:analytics_sdk/entity/ad_impression_event.dart';
import 'package:analytics_sdk/entity/advertising_event.dart';
import 'package:analytics_sdk/enum/user_type_enum.dart';
import 'package:analytics_sdk/enum/video_event_enum.dart';
import 'package:analytics_sdk/enum/ad_type_enum.dart';
import 'package:analytics_sdk/enum/banner_event_enum.dart';
import 'package:analytics_sdk/manager/page_name_manager.dart';
import 'package:analytics_sdk/observer/page_lifecycle_observer.dart';
import 'package:analytics_sdk/widget/global_click_wrapper.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 可选:调整 SDK 默认参数
SdkConfig.configure(
uploadInterval: const Duration(seconds: 10),
autoUploadThreshold: 20,
);
// 配置页面名称映射
PageNameMapper.addMappings({
'/': '首页',
'/video_detail': '视频详情页',
'/user_center': '个人中心',
});
// 从自己的服务端获取加密配置
final encryptedConfig = await yourApiClient.fetchEncryptedConfig();
// 初始化 SDK(设备信息自动采集,无需外部传入)
await AnalyticsSdk.instance.init(
appId: 'your_app_id',
encryptedConfig: encryptedConfig,
channel: 'official',
uid: '', // 未登录时传空
deviceId: 'device_unique_id',
enableDebugBanner: true, // 仅 debug 包生效
);
// 设置登录用户信息(已登录时)
AnalyticsSdk.setUserIdAndType(
userId: 'user_123',
userTypeEnum: UserTypeEnum.vip,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return AnalyticsDebugBanner( // 仅 debug 包显示调试悬浮条
child: MaterialApp(
navigatorObservers: [
AnalyticsSdk.instance.pageObserver, // 页面生命周期自动埋点
],
builder: (context, child) {
return GlobalClickWrapper( // 全局点击自动抓取
child: child!,
);
},
home: const HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () {
AnalyticsSdk.instance.track(VideoEvent(
videoId: 'video_123',
videoTitle: '示例视频',
videoTypeId: 'type_1',
videoTypeName: '娱乐',
videoTagKey: 'hot',
videoTagName: '热门',
videoDuration: 300,
playDuration: 60,
playProgress: 20,
videoBehavior: VideoEventEnum.VIDEO_PLAY,
));
},
child: const Text('上报视频播放'),
),
ElevatedButton(
onPressed: () {
AnalyticsSdk.instance.track(AdImpressionEvent(
pageKey: PageLifecycleObserver.currentPageKey,
pageName: PageNameMapper.getPageName(PageLifecycleObserver.currentPageKey),
adSlotKey: 'home_banner_1',
adSlotName: '首页顶部Banner',
adId: 'ad_123,ad_456',
adType: AdTypeEnum.banner,
));
},
child: const Text('上报广告展示'),
),
ElevatedButton(
onPressed: () {
AnalyticsSdk.instance.track(AdvertisingEvent(
eventType: BannerEventEnum.CLICK.label,
advertisingKey: 'home_popup',
advertisingName: '首页弹窗',
advertisingId: 'adv_001',
));
},
child: const Text('上报弹窗点击'),
),
ElevatedButton(
onPressed: () => AnalyticsSdk.instance.flush(),
child: const Text('立即上报'),
),
],
),
),
);
}
}
// 应用退出时清理资源
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
AnalyticsSdk.instance.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.detached) {
AnalyticsSdk.instance.dispose();
}
}
}
```
---
## 枚举类型参考
### UserTypeEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `UserTypeEnum.normal` | `normal` | 普通用户 |
| `UserTypeEnum.vip` | `vip` | VIP 用户 |
### DeviceEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `DeviceEnum.android` | `Android` | Android 设备 |
| `DeviceEnum.iOS` | `iOS` | iOS 设备 |
| `DeviceEnum.pc` | `PC` | PC 设备 |
### VideoEventEnum
| 枚举值 | key | 说明 |
|--------|-----|------|
| `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` | 读完 |
### AdTypeEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `AdTypeEnum.icon` | `icon` | 图标广告 |
| `AdTypeEnum.banner` | `banner` | Banner |
| `AdTypeEnum.feed` | `feed` | 信息流 |
| `AdTypeEnum.player` | `player` | 播放器广告 |
| `AdTypeEnum.splash` | `splash` | 开屏广告 |
| `AdTypeEnum.text` | `text` | 文字广告 |
### BannerEventEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `BannerEventEnum.CLICK` | `click` | 点击 |
| `BannerEventEnum.CLOSE` | `close` | 关闭 |
| `BannerEventEnum.SHOW` | `show` | 展示 |
### AuthTypeEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `AuthTypeEnum.phone` | `phone` | 手机号 |
| `AuthTypeEnum.deviceid` | `deviceid` | 设备 ID |
| `AuthTypeEnum.email` | `email` | 邮箱 |
| `AuthTypeEnum.username` | `username` | 账号 |
### ClickItemTypeEnum
| 枚举值 | key | 说明 |
|--------|-----|------|
| `ClickItemTypeEnum.VIDEO` | `video` | 视频 |
| `ClickItemTypeEnum.NOVEL` | `novel` | 小说 |
| `ClickItemTypeEnum.COMIC` | `comic` | 漫画 |
### RecommendContentTypeEnum
| 枚举值 | label | 说明 |
|--------|-------|------|
| `RecommendContentTypeEnum.video` | `video` | 视频 |
| `RecommendContentTypeEnum.novel` | `novel` | 小说 |
| `RecommendContentTypeEnum.comic` | `comic` | 漫画 |
### 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` | 内容购买 |
---
## 注意事项
1. **初始化时机**:建议在 `main()` 函数中、`runApp()` 之前初始化 SDK。
2. **encryptedConfig 由宿主 App 负责获取**:SDK 不自主发起配置接口请求,宿主 App 需自行从服务端获取加密配置串,再传入 `init()` 或 `refreshDomainConfig()`。
3. **设备信息自动采集**:品牌、型号、系统名称和版本由 SDK 通过 `device_info_plus` 自动读取,无需外部传入。`deviceId` 仍由宿主 App 提供(Android IMEI/OAID、iOS IDFV 等)。
4. **页面路由命名**:确保所有页面路由都有名称,SDK 才能正确追踪页面生命周期:
```dart
MaterialApp(
routes: {
'/': (context) => HomePage(), // ✅ 有路由名称
'/detail': (context) => DetailPage(),
},
)
```
5. **用户信息更新**:用户登录、登出、升级会员等操作时,记得调用相应的用户管理方法。
6. **网络权限**:
- Android:在 `AndroidManifest.xml` 中添加 `INTERNET` 权限
- iOS:确保 `Info.plist` 中 `NSAppTransportSecurity` 配置正确
7. **资源清理**:应用退出前建议调用 `dispose()` 清理资源。
8. **事件过滤**:未在 `encryptedConfig.eventList` 中启用的事件类型会被静默丢弃,可通过日志确认。`eventList` 为空时默认允许所有事件上报。
9. **AdImpressionEvent 的 adType 需传枚举**:不能传字符串,需使用 `AdTypeEnum.banner` 等枚举值。
---
## 调试
### AnalyticsDebugBanner
在 `init()` 时设置 `enableDebugBanner: true`,并在 Widget 树根部加入 `AnalyticsDebugBanner`(仅 debug 包显示):
```dart
import 'package:analytics_sdk/analytics_sdk.dart';
AnalyticsDebugBanner(
child: MaterialApp(...),
)
```
悬浮条显示 7 个埋点流程步骤:①域名配置 → ②SDK初始化 → ③域名列表 → ④域名测速 → ⑤上报地址 → ⑥待上报队列 → ⑦最近上报结果。
### 通过代码获取调试信息
```dart
// 获取摘要
final info = AnalyticsSdk.instance.getDebugInfo();
print(info['inited']); // SDK 是否初始化
print(info['queueLength']); // 当前队列长度
print(info['reportUrl']); // 上报 URL
// 获取详细步骤
final steps = AnalyticsSdk.instance.getDebugSteps();
for (final step in steps) {
print('${step['name']}: ${step['status']} - ${step['detail']}');
}
```
### 控制日志输出
```dart
import 'package:analytics_sdk/utils/logger.dart';
Logger.enabled = false; // 关闭所有日志
Logger.enabled = true; // 开启日志(debug 包默认已开启)
```
---
## 常见问题 FAQ
**Q: 事件没有上报成功怎么办?**
A:
1. 检查网络连接和权限
2. 确认 `encryptedConfig` 是否传入且格式正确(可用 `validateEncryptedConfig()` 验证)
3. 查看控制台日志了解具体错误
4. 检查是否调用了 `flush()` 或在应用退出前调用了 `dispose()`
**Q: 如何验证 encryptedConfig 是否正确?**
A: 在 debug 包中调用:
```dart
final result = AnalyticsSdk.instance.validateEncryptedConfig(encryptedString);
print(result);
```
**Q: 事件上报延迟多久?**
A: 正常情况下:
- 队列达到 10 条时立即上报
- 每 5 秒定时上报一次
- 调用 `flush()` 立即上报
**Q: 为什么某些事件没有上报?**
A: 可能原因:
1. 事件类型不在 `encryptedConfig.eventList` 白名单中
2. 广告展示事件被 session 级别去重过滤
3. 事件单条超过 300KB 被丢弃
4. 队列满(默认上限 5000 条)
5. 上报 URL 未就绪(域名测速未完成或 encryptedConfig 未传入)
**Q: 如何动态开启/关闭某些事件的上报?**
A: 通过服务端更新 `encryptedConfig` 中的 `eventList`,然后调用:
```dart
await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: newEncryptedConfig);
```
**Q: 如何自定义 SDK 配置参数?**
A:
```dart
import 'package:analytics_sdk/config/sdk_config.dart';
SdkConfig.configure(
maxQueueSize: 10000,
uploadInterval: const Duration(seconds: 30),
connectionTimeout: const Duration(seconds: 15),
);
await AnalyticsSdk.instance.init(...);
```
**Q: 如何控制日志输出?**
A:
```dart
import 'package:analytics_sdk/utils/logger.dart';
Logger.enabled = false;
```
---
## 支持的事件类型
| 事件类型字符串 | 说明 | 上报方式 |
|--------------|------|---------|
| `app_install` | App 安装 | 客户端 |
| `navigation` | 导航事件 | **自动** |
| `app_page_view` | 应用页面展示 | **自动** |
| `page_click` | 应用页面点击 | **自动** |
| `page_lifecycle` | 页面生命周期 | **自动** |
| `advertising` | APP 广告行为(弹窗/运营 Banner) | 客户端 |
| `ad_click` | 广告点击 | 客户端 |
| `ad_impression` | 广告展示 | 客户端 |
| `video_event` | 视频行为 | 客户端 |
| `novel_event` | 小说阅读 | 客户端 |
| `comic_event` | 漫画阅读 | 客户端 |
| `keyword_search` | 关键词搜索 | 客户端 |
| `keyword_click` | 关键词搜索结果点击 | 客户端 |
| `recommend_list_view` | 推荐列表展示 | 客户端 |
| `recommend_list_click` | 推荐列表点击 | 客户端 |
---
## 依赖项
- `flutter`: Flutter SDK (>=3.3.0)
- `uuid`: UUID 生成(设备指纹因子全空时的降级兜底)
- `path_provider: ^2.1.4`: 本地文件路径获取
- `synchronized: ^3.1.0`: 并发控制锁
- `device_info_plus: ^10.0.0`: 自动采集设备品牌、型号、系统名称与版本
- `pointycastle: 3.7.3`: AES-GCM 解密
- `crypto: ^3.0.2`: SHA-256 / MD5 哈希计算
## 环境要求
- Dart SDK: `>=3.4.4 <4.0.0`
- Flutter: `>=3.3.0`