38 · 埋点与行为分析
一句话定位:埋点是"产品的事实数据层",对应前端接入 GA/友盟等统计 SDK。本篇讲 Flutter 里埋点怎么设计、怎么和 Riverpod 打通、怎么保证不漏不重,以及国内隐私合规的红线。
开篇速览
| 问题 | 答案 |
|---|---|
| 前端对应物 | GA / 友盟 / 自研埋点 SDK,dataLayer.push 事件队列 |
| 核心概念 | 事件总线 + 埋点 SDK + 批量上报 + 离线补偿 |
| 最常见的错 | 埋点散落在每个页面手写,字段不统一,漏埋难查 |
| 一句话红线 | 埋点不是"加一行日志",而是有 schema、有去重、有隐私边界的数据管道 |
代码来源说明:HaierEnergy 当前
lib/有零散日志上报痕迹,但无统一埋点框架。 本篇按"通用方案 + 项目建议"撰写。
一、埋点的四类事件
| 类型 | 例子 | 关注指标 |
|---|---|---|
| 页面(PV/UV) | 进入设备详情页 | 访问量、停留时长 |
| 点击/行为 | 点击"下发参数" | 点击率、转化率 |
| 曝光 | 列表项可见 | 曝光率、有效曝光 |
| 异常/性能 | 接口失败、卡顿 | 错误率、可用性(与 20 篇 联动) |
二、埋点架构:事件总线 + 采集层
埋点代码不该散落在业务里。推荐分层:
dart
// 1. 埋点模型(统一 schema,禁止各页面自定义字段名)
class AnalyticsEvent {
final String name; // 如 device_detail_view
final Map<String, Object?> params; // 规范化字段
final DateTime occurredAt;
}
// 2. 事件总线:业务方只管发事件,不管上报细节
class AnalyticsBus {
void track(String name, {Map<String, Object?> params = const {}}) {
_controller.add(AnalyticsEvent(name: name, params: params));
}
}2.1 与 Riverpod 打通
建议把埋点封装成 Provider,业务代码一行调用:
dart
final analyticsProvider = Provider<AnalyticsBus>((ref) => AnalyticsBus());
// 业务侧
ref.read(analyticsProvider).track('device_param_send', params: {
'device_id': deviceId,
'result': result ? 'ok' : 'fail',
});为什么用 Provider 而不是全局单例:可注入、可测试、可替换成 mock(配合 19 篇 的 ProviderContainer.overrides)。
三、批量上报与离线补偿
高频事件不能一条一条发 HTTP:
| 手段 | 作用 |
|---|---|
| 批量聚合 | 攒 N 条或 T 秒再上报 |
| 本地缓存队列 | 网络失败先落盘,恢复后补发 |
| 去重 | 相同事件在短时间内只算一次 |
| 采样 | 超大流量场景按比例抽(配置化) |
dart
// 伪代码:批量上报器
class BatchReporter {
final _buffer = <AnalyticsEvent>[];
void enqueue(AnalyticsEvent event) {
_buffer.add(event);
if (_buffer.length >= 20) flush();
}
Future<void> flush() async {
final batch = List.of(_buffer);
_buffer.clear();
await api.report(batch); // 失败时写回缓存,下次重试
}
}四、关键设计点
4.1 字段规范化
所有事件统一携带 app_version、os、device_id、session_id、ts。禁止同一个语义在不同页面叫不同字段名。
4.2 会话(Session)
一次会话要有唯一 session_id,前后台切换超过阈值(如 30 分钟)切新会话。埋点数据没有会话概念就无法算漏斗。
4.3 曝光埋点的坑
曝光 ≠ 进入页面。列表项进入可视区才算曝光:
dart
// 用 VisibilityDetector 或自定义滚动回调,判定可见后再上报
VisibilityDetector(
key: const ValueKey('device_card_1'),
onVisibilityChanged: (info) {
if (info.visibleFraction > 0.5 && !_reported) {
analytics.track('device_card_expose', params: {'device_id': id});
}
},
child: deviceCard,
)五、隐私合规红线(必读)
| 要求 | 做法 |
|---|---|
| 告知与授权 | 首次启动弹隐私协议,用户同意后才初始化埋点 |
| 最小化采集 | 只采业务必要字段,不采无关通讯录/位置 |
| 标识脱敏 | device_id 用匿名化标识,不绑定可识别个人信息 |
| 可关闭 | 提供"关闭个性化统计"开关,关闭后立即停采 |
| 跨境合规 | 数据出境需评估(个人信息保护法 / GDPR) |
埋点 SDK 的初始化时机也要合规:很多 SDK 在 main() 里直接初始化,会违反"先授权后采集"。
六、项目落地建议
- 先定事件字典(Excel/文档维护),再写代码,避免字段发散;
- 埋点抽象成
AnalyticsBusProvider,业务只发事件不碰上报; - 曝光/点击/页面三类高频事件必须用批量 + 去重;
- 隐私授权流程走通后再初始化第三方 SDK;
- 与 20 篇 的性能/异常数据共用一套上报通道,减少维护面。
动手练习
- 设计
AnalyticsBus+BatchReporter的最小实现(含批量、失败缓存、去重); - 给设备列表页加曝光埋点,用 Widget test 验证"滚动到可见才上报";
- 写一份 10 行以内的事件字典表(页面/点击/曝光/异常各 2~3 条)。
自检清单
- [ ] 能说出四类事件与各自关注指标
- [ ] 能用事件总线 + Provider 统一埋点入口
- [ ] 知道批量上报、去重、离线补偿的作用
- [ ] 知道曝光 ≠ 进入页面,会用可见性判定
- [ ] 知道隐私合规的最小动作(授权 / 最小化 / 可关闭)
常见误区
- "埋点就是 console.log":无 schema、无去重、无缓存,数据没法用;
- "曝光就是进页面":列表项要滚动到可视区才算曝光;
- "直接初始化第三方 SDK":先授权后采集,顺序反了就是合规事故;
- "一条一条发请求":高频事件会把请求打爆,必须批量。
相关文档
- 00 · 学习路线图 —— 阅读顺序
- 20 · 异常捕获与错误监控 —— 异常/性能数据与埋点共用通道
- 19 · 测试实战 —— 用
ProviderContainer.overrides测试埋点 - 08 · 状态管理 Riverpod —— Provider 注入方式
- 34 · 动态化与热更新 —— 灰度开关需要埋点支撑