Files
hjsq_android/analytics_sdk

版本说明: 本分支(dev)适用于 Flutter 3.0.5 及以上(Dart >=2.17)。 如使用 Flutter 1.22.5,请切换至 compat/flutter-1.22 分支。

Analytics SDK

一个 Flutter 埋点 SDK,用于采集 App 内页面、点击、广告、视频、小说、漫画、搜索、推荐等行为,并提供批量上报、本地持久化、域名测速和事件类型动态配置能力。

目录

功能特性

  • 多事件类型:页面浏览、页面点击、导航、广告展示/点击、视频/小说/漫画行为、关键词搜索、推荐等事件
  • 自动埋点:页面生命周期(曝光/停留)、全局点击(默认开启)
  • 批量上报:队列聚合,默认每 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 中添加依赖(示例):

dependencies:
  analytics_sdk:
    path: ../analytics_sdk  # 本地导入:根据实际目录调整相对路径

然后执行:

flutter pub get

快速开始

1. 初始化 SDK

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,再从自己配置接口拿到加密串,可以这样用:

// 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. 页面生命周期 & 全局点击埋点

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

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

static Map<String, dynamic> getParams([List<String>? keys])

获取 SDK 当前全部公共参数(或按 key 过滤子集),可用于服务端事件拼装或调试:

// 获取全部公共参数
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

Future<void> refreshDomainConfig({String? encryptedConfig})

使用场景:

  • 先调用 init(),再从服务端获取 encryptedConfig;
  • 运行中需要更新域名列表或事件类型白名单。
// 获取到最新加密配置后刷新
final newConfig = await yourApiClient.fetchEncryptedConfig();
await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: newConfig);

传入 null 或空字符串时忽略本次刷新。本方法永远不会抛出异常。

事件上报相关

// 上报内置事件
void track(dynamic event)

// 立即触发上报
Future<void> flush()

// 页面生命周期观察者
NavigatorObserver get pageObserver

// 资源清理(可选)
Future<void> dispose()

track() 支持传入 SDK 内置的事件类(如 VideoEvent、AdImpressionEvent 等)或直接传 Map<String, dynamic>。

SDK 会自动:

  • 做广告展示事件去重与过滤;
  • 检查事件类型是否在白名单中;
  • 校验必填字段;
  • 检查单条事件大小(超过 300KB 丢弃);
  • 将事件写入队列和本地缓存;
  • 在队列长度达到阈值或定时器触发时批量上报。

用户与会话

// 设置用户 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 自动管理,无需手动干预。

获取设备公共字段(服务端复用)

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 内部解密后格式为:

{
  "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() 成功后自动上报。

验证加密配置(调试包)

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() 之前调用:

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)

恢复所有默认值:

SdkConfig.reset();

更多文档

本仓库根目录提供了更详细的开发文档:

  • SDK_USAGE_GUIDE.md:完整使用指南,包含所有内置事件的数据结构与字段说明、会话规则、自动埋点细节、性能与容错机制说明等。

建议接入前先通读 SDK_USAGE_GUIDE.md,本 README 主要用于快速了解和集成。


手动上报事件

void track(dynamic event)

上报任意事件,传入 SDK 内置事件类即可。

1. App 安装事件(客户端)

首次安装启动时,读取剪贴板获取到 traceId 后上报。

AnalyticsSdk.instance.track(AppInstallEvent(
  traceId: 'landing_page_trace_id_xxx',  // 落地页点击生成的唯一 ID
));

2. 视频行为事件(客户端)

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. 小说阅读事件(客户端)

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. 漫画阅读事件(客户端)

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 内只会上报一次。

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. 广告点击事件(客户端)

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 等的展示、点击、关闭行为。

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. 关键词搜索事件(客户端)

AnalyticsSdk.instance.track(KeywordSearchEvent(
  keyword: '示例关键词',
  searchResultCount: 100,
));

9. 关键词搜索结果点击事件(客户端)

import 'package:analytics_sdk/enum/click_item_type_enum.dart';

AnalyticsSdk.instance.track(KeywordClickEvent(
  keyword: '示例关键词',
  clickItemId: 'video_123',
  clickItemType: ClickItemTypeEnum.VIDEO,
  clickPosition: 1,
));

10. 推荐列表展示事件(客户端)

AnalyticsSdk.instance.track(RecommendListViewEvent(
  pageKey: 'home',
  pageName: '首页',
  recommendContentType: RecommendContentTypeEnum.video,
  recommendTraceId: 'rec_trace_abc123',
  recommendId: 'vid_001,vid_002,vid_003',
));

11. 推荐列表点击事件(客户端)

AnalyticsSdk.instance.track(RecommendListClickEvent(
  pageKey: 'home',
  pageName: '首页',
  recommendTraceId: 'rec_trace_abc123',
  recommendId: 'vid_001',
));

强制立即上报

await AnalyticsSdk.instance.flush();

资源清理

建议在应用退出前调用,SDK 会先尝试上报剩余事件,再释放所有资源:

await AnalyticsSdk.instance.dispose();

用户类型管理

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,建议在应用启动时统一注册映射:

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'); // '视频详情页'

动态获取当前页面信息

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:

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

完整示例

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 才能正确追踪页面生命周期:

    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 包显示):

import 'package:analytics_sdk/analytics_sdk.dart';

AnalyticsDebugBanner(
  child: MaterialApp(...),
)

悬浮条显示 7 个埋点流程步骤:①域名配置 → ②SDK初始化 → ③域名列表 → ④域名测速 → ⑤上报地址 → ⑥待上报队列 → ⑦最近上报结果。

通过代码获取调试信息

// 获取摘要
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']}');
}

控制日志输出

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 包中调用:

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,然后调用:

await AnalyticsSdk.instance.refreshDomainConfig(encryptedConfig: newEncryptedConfig);

Q: 如何自定义 SDK 配置参数?

A:

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:

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