埋点数据模型设计:通用事件结构、用户标识与上下文

数据模型是埋点系统的地基。地基没打好,后面所有分析都是空中楼阁。我见过太多团队,埋点上了半年,结果发现用户标识对不上、事件字段乱成一锅粥——说白了,就是一开始没想清楚数据模型该怎么设计。

这一章,我们重点聊三件事:通用事件结构长什么样、用户标识与设备指纹怎么选、事件属性与上下文信息如何组织。这三块搞定了,你的埋点数据就能做到“进来是干净的,出去是可分析的”。

通用事件结构设计

先问一个问题:你收到的埋点数据,是不是每个事件都长一个样?

理想情况下,是的。不管你是点击、曝光、页面浏览还是自定义业务事件,它们都应该共享一套顶层字段结构。我习惯把这套结构叫做“事件信封”——信封的格式统一,里面的内容可以不同。

核心原则:所有事件必须包含以下四类字段——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 被法务找去喝茶,从那以后,所有网络字段都做了脱敏处理。

数据模型整体架构

最后,我用一张图来总结这一章的核心内容。这张图展示了事件数据模型的整体架构,从顶层结构到底层字段,一目了然。

事件数据模型整体架构 事件信封(Event Envelope) event_id event_type timestamp user properties user_id device_id anonymous_id 业务属性 context page device network url referrer title os screen type carrier 设计要点总结 ① 所有事件共享同一套顶层结构(事件信封) ② 用户标识分三层:user_id / device_id / anonymous_id ③ 属性分三类:公共属性(context)/ 业务属性(properties)/ 扩展属性(ext)

这张图把整个数据模型串起来了。你从上往下看,先有事件信封,再拆出核心字段,然后 user、context、properties 各自展开。每一层都有明确的职责,不会出现字段归属混乱的情况。