埋点数据模型设计:通用事件结构、用户标识与上下文
数据模型是埋点系统的地基。地基没打好,后面所有分析都是空中楼阁。我见过太多团队,埋点上了半年,结果发现用户标识对不上、事件字段乱成一锅粥——说白了,就是一开始没想清楚数据模型该怎么设计。
这一章,我们重点聊三件事:通用事件结构长什么样、用户标识与设备指纹怎么选、事件属性与上下文信息如何组织。这三块搞定了,你的埋点数据就能做到“进来是干净的,出去是可分析的”。
通用事件结构设计
先问一个问题:你收到的埋点数据,是不是每个事件都长一个样?
理想情况下,是的。不管你是点击、曝光、页面浏览还是自定义业务事件,它们都应该共享一套顶层字段结构。我习惯把这套结构叫做“事件信封”——信封的格式统一,里面的内容可以不同。
核心原则:所有事件必须包含以下四类字段——event_id(事件唯一标识)、event_type(事件类型)、timestamp(事件发生时间)、user(用户信息)。其余字段按需扩展。
来看一个具体的通用事件结构示例:
{
"event_id": "evt_20250321_8a3f9c2b",
"event_type": "page_view",
"event_name": "商品详情页浏览",
"timestamp": 1742544000000,
"user": {
"user_id": "u_10086",
"device_id": "d_abc123",
"anonymous_id": "anon_xyz789"
},
"context": {
"page": {
"url": "/product/12345",
"referrer": "/search?q=手机",
"title": "iPhone 16 Pro 详情页"
},
"device": {
"os": "iOS 18.2",
"browser": "Safari",
"screen_width": 390,
"screen_height": 844
},
"network": {
"type": "wifi",
"carrier": "中国移动"
}
},
"properties": {
"product_id": "12345",
"product_name": "iPhone 16 Pro",
"price": 8999.00,
"category": "手机数码"
}
}
这个结构里,event_id 是全局唯一的,用来去重和追踪。我遇到过线上重复上报的情况,如果没有 event_id,你根本不知道哪条是重复的。所以,event_id 必须由客户端生成,且带上时间戳和随机因子。
event_type 和 event_name 的区别是什么?event_type 是机器用的枚举值,比如 page_view、click、exposure;event_name 是人读的中文名,方便产品经理和运营同学看报表时理解。
我的建议:event_type 用下划线命名法,全部小写。event_name 用中文,但不要超过 15 个字。太长的事件名在 BI 工具里显示不全,很痛苦。
用户标识与设备指纹
用户标识是埋点里最容易被搞砸的部分。我见过一个项目,同一个用户在一天内产生了 3 个不同的 user_id,原因是登录态切换时没有处理好匿名 ID 和登录 ID 的映射。
用户标识体系,我把它分为三层:
| 层级 | 标识字段 | 生命周期 | 典型场景 |
|---|---|---|---|
| 第一层 | user_id |
用户登录后存在 | 已登录用户的行为追踪 |
| 第二层 | device_id |
设备级别,卸载重装可能变 | 未登录用户的设备级追踪 |
| 第三层 | anonymous_id |
会话级别,每次启动生成 | 临时匿名会话追踪 |
这三层标识,在事件结构里是同时存在的。用户登录后,user_id 和 anonymous_id 都要上报,这样数据仓库才能做用户关联。
设备指纹这块,我踩过一个坑。早期我们直接用 IDFA 做设备标识,结果 iOS 14 之后用户可以选择限制广告追踪,大量设备指纹失效。后来我们改用 IDFV + 设备特征组合 的方式,稳定性好了很多。
避坑指南:不要完全依赖单一设备标识。iOS 的 IDFA、Android 的 OAID 都可能被用户关闭。建议用「设备 ID + 浏览器指纹 + 网络特征」做多因子组合,生成一个稳定的设备指纹。
设备指纹的生成逻辑,我一般这样设计:
// 伪代码:设备指纹生成
function generateDeviceFingerprint() {
const idfa = getIDFA(); // iOS 广告标识
const idfv = getIDFV(); // iOS 应用标识
const oaid = getOAID(); // Android 广告标识
const screen = getScreenInfo(); // 屏幕分辨率
const language = getLanguage(); // 系统语言
const timezone = getTimezone(); // 时区
// 优先级:IDFA > IDFV > OAID > 特征组合
if (idfa) return hash(idfa);
if (idfv) return hash(idfv);
if (oaid) return hash(oaid);
// 兜底:用设备特征做哈希
const fallback = `${screen}_${language}_${timezone}`;
return hash(fallback);
}
嗯,这里要注意:设备指纹的哈希算法要稳定,不要每次升级 SDK 就换算法,否则历史数据全对不上了。
事件属性与上下文信息
事件属性和上下文信息,是埋点数据里最灵活、也最容易失控的部分。我见过一个电商项目,properties 字段里塞了 50 多个 key,很多 key 只有一两个事件在用,维护成本极高。
我的做法是:把属性分为三类——公共属性、业务属性、扩展属性。
- 公共属性:所有事件都带,比如页面信息、设备信息、网络信息。这些放在
context对象里。 - 业务属性:特定事件类型才带,比如“加入购物车”事件带
product_id、quantity。这些放在properties对象里。 - 扩展属性:临时调试或 A/B 实验用的字段,用
ext对象包裹,方便后续清理。
来看一个属性分类的表格:
| 属性类别 | 存放位置 | 示例字段 | 生命周期 |
|---|---|---|---|
| 公共属性 | context |
page.url, device.os, network.type | 长期存在,所有事件共享 |
| 业务属性 | properties |
product_id, price, order_status | 随业务迭代增减 |
| 扩展属性 | ext |
experiment_id, debug_flag | 临时使用,定期清理 |
上下文信息里,页面上下文是最容易被忽略的。很多团队只上报了 url,但没上报 referrer 和 title。结果分析用户路径时,根本不知道用户是从哪个页面跳转过来的。
我的经验:页面上下文至少上报三个字段——url(当前页)、referrer(来源页)、title(页面标题)。这三个字段能解决 80% 的路径分析需求。
设备上下文里,screen_width 和 screen_height 一定要上报。为什么?因为你要区分是手机端还是 PC 端的行为。光靠 user_agent 解析太慢,直接上报屏幕尺寸更靠谱。
网络上下文,我建议只上报 type(wifi/4G/5G)和 carrier(运营商)。不要上报 ip,涉及隐私合规问题。我曾经因为上报了 IP 被法务找去喝茶,从那以后,所有网络字段都做了脱敏处理。
数据模型整体架构
最后,我用一张图来总结这一章的核心内容。这张图展示了事件数据模型的整体架构,从顶层结构到底层字段,一目了然。
这张图把整个数据模型串起来了。你从上往下看,先有事件信封,再拆出核心字段,然后 user、context、properties 各自展开。每一层都有明确的职责,不会出现字段归属混乱的情况。