> **版本说明:** 本分支(`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 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 getParams([List? 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 refreshDomainConfig({String? encryptedConfig}) ``` 使用场景: - 先调用 `init()`,再从服务端获取 `encryptedConfig`; - 运行中需要更新域名列表或事件类型白名单。 ```dart // 获取到最新加密配置后刷新 final newConfig = await yourApiClient.fetchEncryptedConfig(); await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: newConfig); ``` > 传入 null 或空字符串时忽略本次刷新。本方法永远不会抛出异常。 ### 事件上报相关 ```dart // 上报内置事件 void track(dynamic event) // 立即触发上报 Future flush() // 页面生命周期观察者 NavigatorObserver get pageObserver // 资源清理(可选) Future dispose() ``` `track()` 支持传入 SDK 内置的事件类(如 `VideoEvent`、`AdImpressionEvent` 等)或直接传 `Map`。 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 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 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 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`