# 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 _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`,包含:`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 adIds) { AnalyticsSdk.instance.track(AdImpressionEvent( pageKey: 'home', pageName: '首页', adSlotKey: 'home_feed_ad', adSlotName: '首页信息流广告', adId: adIds.join(','), adType: 'feed', )); } // ——— 应用退出场景 ——— Future onAppExit() async { await AnalyticsSdk.instance.flush(); await AnalyticsSdk.instance.dispose(); } ```