SDK 写好以后,我为什么没有直接换掉旧的埋点上报代码?

3 阅读10分钟

上一篇我讲了这周为什么要把项目里的埋点能力整理成 SDK。

这篇只看一个更具体的问题:

SDK 已经能生成埋点请求以后,为什么我没有马上把项目里原来的上报代码换掉?

一开始 SDK 第一版能生成请求数据时,我很容易想到下一步:既然新 SDK 已经能生成请求,那就把旧上报代码换掉,但是动手之前,我停了一下。

旧代码已经跟着当前 App 的埋点功能跑过、验证过,也已经在线上版本里的进行埋点上报,所以,新 SDK 只要有一个字段、一个时间值、一次失败重试和旧实现不一样,后面的埋点数据分析就可能被带偏,影响运营的决策。

所以我没有马上替换,我先做了一件更稳的事:同一次业务调用里,让旧实现和 SDK 各生成一份请求数据,先把两边放在一起对比。

旧上报代码不能当成普通旧代码直接替换

这次要换掉的内容,比一个上报方法多得多。

在当前项目里,SC_MQ09AnalyticsManager 负责的事情很多:

  • 事件请求数据怎么生成
  • 用户属性请求数据怎么生成
  • 公共属性什么时候取
  • 时间字段怎么生成
  • 失败后怎么重试
  • user_setOnce 成功后怎么标记状态

这些东西已经跟真实业务跑在一起了。

所以 SDK 写出来以后,我要先确认两件事:SDK 能不能完成上报;同一次业务调用下,SDK 生成的请求数据和旧代码原来生成的数据能不能对上。

请求能上报成功,只说明最基本的上报能力具备了。

我更关心的是:同一个事件,如果以前由旧代码上报,现在换成 SDK 上报,两份请求数据是不是完全一致。

如果只是少一个字段,问题还比较容易发现。

更麻烦的是另一类情况:字段都在,格式也对,但当时那一次事件里取到的值不一样,比如设备内存、磁盘、帧率、网络状态这类公共属性,本来就会随着设备状态变化。

这也是我没有马上替换的原因。

旧代码能跑,新 SDK 也能跑,中间还差一步:我要先看到两边生成出来的数据都能对上。

我先让 SDK 生成一份对照用的请求数据

当时我先做了 mirror

这里的 mirror 可以直接理解成:同一次业务调用里,旧代码照常生成并上报原来的请求数据,SDK 也按同样的输入生成一份请求数据。但 SDK 这份数据先不上报,只打印出来做对比。

大概是这样:

同一次业务调用
    |
    |-- 旧实现:生成请求数据 -> 实际上报
    |
    |-- SDK:生成请求数据 -> 只打印日志,不上报

这样做有一个好处:当前 App 的线上行为不会被影响。

旧代码继续负责实际上报,SDK 只负责生成一份对照数据。这样日志里能同时看到旧请求数据和 SDK 请求数据,我可以进行逐项比较:

  • 事件名是否一致
  • 业务字段是否一致
  • 用户属性字段是否一致
  • 时间字段是否一致
  • 公共属性是否一致
  • 失败重试时用的请求数据是否一致
  • 上报的属性的值是否一致

这一步很简单,也很有用。

因为它把“我觉得 SDK 应该没问题”变成了“我能在日志里看到两份请求数据到底差在哪里”。

当时对比下来,大部分业务字段都能对上,包括 positionsexageheightweight、固定的用户属性这部分,但公共属性这部分,还是暴露出了问题。

对比时麻烦点出在会变化的公共属性上

第一轮日志对比时,我看到一些字段还需要继续向旧的埋点代码对齐,比如:

  • #ram
  • #disk
  • #fps
  • #network_type
  • #device_model
  • time

这些问题后来陆续解决了。

比如 SDK 里通过 ZZHAnalyticsThinkingPropertyBridge 去复用 ThinkingDataCore 的设备属性能力;事件时间也改成同一次调用里先固定一个 resolvedTimestamp,旧的埋点代码和 SDK 都用这个时间,避免毫秒级差异。

做到后面,26 份请求数据里只剩 2 份 #ram0.1GB 差异。

这个差异很小,#ram0.1GB,原因不在属性取值错了,而在🌧️取值次数。

旧的埋点代码中取了一次内存值,SDK 又取了一次,两次取值之间隔了很短一段时间,设备状态已经变了。

可以简单理解成这样:

10:00:01  旧实现读取 #ram = 3.2GB
10:00:01  SDK 再读取 #ram = 3.3GB

字段没写错,但两次读取的设备状态已经不同。

这类问题很容易被忽略,因为你看字段名,它是对的;看字段类型,它也是对的;看数值差异,也不算大。

但如果我要验证“同一个事件换成 SDK 以后上报的数据是否一致”,这种差异就不能放过。

同一个埋点事件里,公共属性最好只取一次,如果旧的埋点代码取一次,SDK 再取一次,两份请求数据就可能因为设备状态变化出现差异。哪怕差异很小,也会让后面的验证不通过。

所以我改了做法:SDK 停止重新采集这类动态公共属性,改为复用第一次已经生成好的那份请求数据。

后面补 snapshot,是为了复用第一次生成好的请求数据

后来我补了 snapshot

这里的 snapshot 可以直接理解成:把第一次生成好的那份请求数据保存下来,后面上报也好,失败后重试也好,都不要重新生成一份新的请求数据,继续用第一次那份。

改之前大概是这样:

旧实现取公共属性 -> 生成旧请求数据
SDK 再取公共属性 -> 生成 SDK 请求数据

两边可能出现动态值差异

改之后变成这样:

第一次生成完整请求数据
    |
    |-- SDK 复用这份请求数据
    |-- 实际上报复用这份请求数据
    |-- 失败重试也复用这份请求数据

这一步看起来只是少取了一次公共属性,但它解决的问题很关键。

它让后面的验证对象变得明确:我比较的不再是“两次重新生成出来的请求数据”,后面的上报和重试都围绕第一次已经生成好的那份请求数据继续处理,这对失败重试也很重要。

如果第一次请求失败后,重试时重新生成请求数据,那么重试上报的数据可能已经不是第一次失败时那份数据了,时间、内存、网络状态都有可能变化,特别是上报的时间,产品运营对于这个上报时间很看重。

所以我后面就按这个规则来做:

第一次生成好的请求数据,就是后面上报和重试的基准。

这样一来,请求失败后要不要重试,是另一个问题;但重试时上报的内容,应该还是第一次那份。

请求数据稳定以后,才切到 sdkSendOnly

等请求数据对比基本稳定后,才进入 sdkSendOnly

sdkSendOnly 的意思很简单:同一个埋点请求,后面不再由旧上报代码处理,实际上报这一步改由 SDK 的 transport 完成。

当时切换前,我至少确认了这几件事:

  • SDK 生成的请求数据已经和旧的埋点代码对齐
  • 动态公共属性不再重复采集
  • SDK transport 已经接上当前项目原来的 event / user property URL
  • 请求 header 仍然带上当前项目需要的 pkg(包名)
  • 失败后仍然写入现有 retry store
  • 用户属性成功态仍然由 manager 标记,避免把状态逻辑一起改乱

这一步最重要的地方是:我没有把所有东西都一起改。

实际上报这一步改成由 SDK 完成,但当前项目里原来已经跑通的状态处理、失败缓存、用户属性成功标记,仍然按原来的方式保留。

如果用表格看,会更清楚:

阶段旧实现做什么SDK 做什么实际由谁上报
mirrorOnly生成并上报原请求生成对照请求,只打印旧实现
sdkSendOnly保留必要状态处理生成并上报请求SDK
失败重试继续使用现有重试存储复用第一次请求数据现有重试存储

切到 sdkSendOnly 后,我又看了一轮日志。

当时的验证结果是:

  • snapshot_event_total = 22
  • snapshot_user_total = 3
  • transport_send_total = 25
  • transport_success_total = 25
  • transport_fail_total = 0
  • mq_send_total = 0
  • mismatch_total = 0

关键事件也都过了,包括:

  • ta_app_install
  • ta_app_start
  • start_page_show
  • first_page_show
  • ta_app_end
  • user_setOnce

这些数字对我来说很重要。

因为到这一步,我已经在日志里看到了结果:SDK 上报了 25 次,成功 25 次,旧上报代码没有继续上报,数据也没有再出现不一致。

这时候再说“可以把实际上报交给 SDK”,才有底气。

最后清旧代码,是为了避免以后又回到两套上报实现并存

sdkSendOnly 稳定以后,我没有把验收期的调试代码一直留着。

当时清掉了这些东西:

  • dual-send 临时强制分支
  • ZZHAnalyticsDebugProbe 调用
  • sdk send dispatch 临时日志
  • fake account_id 测试开关
  • probe reset / summary 调用
  • transport 里的 probe 计数

我清这些代码,主要是为了避免后面排查时分不清当前到底应该看哪套实现。

尤其是埋点这种问题,一旦出了字段差异,很难接受“可能是旧代码上报的,也可能是新 SDK 上报的”这种状态。

所以当 SDK 上报已经稳定,旧上报代码不再负责实际上报后,临时对比和调试分支就应该尽快退出。

保留必要的兼容就可以了,但默认实现应该只剩一套。

后面的人接手时,也应该能很快看懂:

  • 当前默认由 SDK 负责实际上报
  • 失败后仍然复用第一次生成好的请求数据
  • 用户属性成功态仍然按当前项目原来的规则处理
  • 排查问题时,应该看 SDK 的上报日志和当前项目的 retry store

这比留下两套都能跑的上报代码更可靠。

写到这里

这次替换埋点上报代码,我确认的一点是:不能靠“新 SDK 能上报请求”来判断能不能替换旧代码。

我需要先看到同一次业务调用下,新旧两份请求数据对得上,也要确认失败重试用的还是第一次那份数据。等这些都稳定以后,再把实际上报交给 SDK,才算是真的把旧上报代码换掉。

这个顺序看起来慢一点,但它让我少踩了很多坑。

旧代码被换掉之前,我已经在日志里看到两边请求数据对齐,也看到 SDK 实际上报成功。

对这种已经在线上跑过的埋点上报代码来说,这比“代码结构更好看”重要得多。