2. 埋点数据模型设计:事件结构、序列化、时间戳与优先级字段

埋点数据模型是上报系统的核心骨架。一个设计合理的数据模型应当同时满足传输效率、持久化可回溯和重试优先级判定三种需求。本章从事件结构定义出发,逐步分析序列化选型、时间戳规范以及优先级字段在重试场景中的关键作用。

2.1 事件结构:最小必要字段集

一个埋点事件至少包含以下6 个必填字段,其余业务属性以 Map 形式附加:

字段类型说明重试中的角色
event_idString (UUID)全局唯一标识,去重判重依据幂等性保证
event_nameString事件名称,如 "button_click"路由/分组
user_idString用户标识(匿名或登录)数据归属
timestampLong (毫秒)事件发生时的 UTC 时间戳时效性判断
priorityInt0 = 低, 1 = 普通, 2 = 高重试队列优先级排序
propertiesMap<String, Any>业务自定义属性序列化负载核心
📌 设计原则:禁止在 properties 中嵌套 event_id / priority 等元数据,避免序列化冗余与字段冲突。

2.2 序列化方案:JSON vs 二进制

埋点上报通常选用 JSON 作为序列化格式,原因在于:

  • 自描述:无需额外 schema 即可解析,便于调试与动态打点;
  • Kotlin 生态:kotlinx.serialization 提供编译期安全、零反射序列化;
  • 重试场景:JSON 字符串可直接存于本地数据库或文件,无需反序列化即可查看内容。

当对体积有极致要求时(如弱网大量缓存),可降级为 Protocol Buffers,但会增加 debug 成本。

序列化示例:kotlinx.serialization

@Serializable
data class AnalyticsEvent(
    @SerialName("id") val eventId: String,
    @SerialName("name") val eventName: String,
    @SerialName("uid") val userId: String,
    @SerialName("ts") val timestamp: Long,
    @SerialName("p") val priority: Int = 1,
    @SerialName("props") val properties: Map<String, @Serializable Any> = emptyMap()
)

// 序列化
val event = AnalyticsEvent(
    eventId = UUID.randomUUID().toString(),
    eventName = "screen_view",
    userId = "u_abc123",
    timestamp = System.currentTimeMillis(),
    priority = 1,
    properties = mapOf("screen" to "home", "duration" to 5000)
)
val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }
val payload: String = json.encodeToString(event)

2.3 时间戳规范:UTC 毫秒级 + 本地时钟偏移记录

上报系统的重试逻辑高度依赖时间戳:

  • 事件时间戳:客户端产生事件时的 UTC 毫秒数,用于时效性过滤(超过 TTL 的事件直接丢弃,不再重试);
  • 本地时间偏移:客户端将 System.currentTimeMillis() 与 SystemClock.elapsedRealtime() 做差值,记录偏移量。服务端在收到事件后可反推设备真实时钟偏差,辅助判断重试窗口。
⚠️ 常见错误:直接使用 System.currentTimeMillis() 作为重试间隔计算依据,当用户修改系统时间后会导致重试延迟被异常缩短或拉长。应始终使用 elapsedRealtime 计算重试间隔,事件时间戳仅用于业务统计。

2.4 优先级字段:重试队列调度的核心

priority 字段控制事件在上报队列中的处理顺序和服务端的重试优先级:

优先级值含义重试行为
0 (Low)统计无关痛痒,如冗余日志不上报重试,缓存满时优先丢弃
1 (Normal)常规业务事件,如页面浏览最多重试 3 次,指数退避间隔 2~32s
2 (High)关键交易/计费事件无限重试(直到成功或业务废弃),退避间隔 1~60s,可触发客户端弹窗提示用户

在序列化负载中,priority 同时写入 event 对象的顶层字段,方便重试中间件(如本地 PriorityQueue 或 Room 的 ORDER BY)直接排序。

2.5 数据模型整体结构图

下图展示了一个完整埋点事件从构造到序列化的数据流动与字段分组:

埋点事件数据模型结构 AnalyticsEvent event_id: String user_id: String priority: Int event_name: String timestamp: Long properties: Map 序列化 → JSON String (kotlinx.serialization) priority 同时参与重试队列排序和时效性过滤 timestamp 用于判断事件是否过期(TTL)

2.6 稳定性优化:空值与默认值保护

在埋点 SDK 中,数据模型应避免空指针和非法值导致序列化崩溃:

  • event_id 使用 UUID.randomUUID().toString() 自动生成兜底;
  • timestamp 为 Long 类型,禁止使用 Long?,避免重试时出现 null 时间戳;
  • properties 提供 MutableMap() 并内联 .toMap() 防止外部修改。
📘 调试建议:在开发阶段,打印 json.encodeToString(event) 的结果,核对每个字段名称和类型是否符合服务器契约。同时检查 priority 字段是否被误编码为字符串 "1" 而非整型 1。

2.7 常见错误自查

错误后果解决方案
timestamp 使用本地时区而非 UTC服务端重试窗口计算错误始终通过 System.currentTimeMillis() 获取 UTC 时间
priority 未写入序列化 payload重试中间件无法排序在 AnalyticsEvent 中保留 @SerialName("p")
properties 包含不可序列化类型(如 View)JsonSerializationException 导致事件丢失使用 @Serializable 包装类或只允许基本类型、String、List、Map
事件 ID 重复重试去重失败,产生重复数据使用 UUID 并确保构造时新鲜生成