YSS

Write Less & Do More

埋点现状梳理及解决

埋点历史

要了解现状就必须知道之前埋点的历史是怎样的。

我们现在主要使用的埋点版本是 V2,追溯到最早的版本是 V0 。

BI Tracking V0

V0 版本是 2015 年开发的,现在在 Confluence 上已经找不到相关文档了。

它是一个没有固定格式约定(schema-less)的,可以按照自己的需要发送任意的数据。

对于使用者来说非常的灵活,但是对于最终的数据处理用户,也就是我们的 BI 团队来说,这个是巨大的痛苦。每来一个需求就意味需要重新去写程序处理,特别是需要花费大量的精力去预处理这些数据。

这在业务没有大规模扩大的情况下,总还是能跑的通的,但是随着 Shopee 业务的极速发展,这样的方式不管是对于 BI 团队来说,还业务团队来说,都会有巨大的成本。

所以,有了 V1 版本。

BI Tracking V1

首先,V1 版本相比 V0 的第一个差异就是使用了 protobuffer,这个可以极大压缩传输数据量大小。

而使用了 protobuffer,就需要定义一个固定格式的 Schema,现有的定义可以参考://

然后,在实施过程中,所有的 TrackingEvent 都需要 BI 团队定义,然后传递给开发人员。

在这么一个工作流程下,数据预处理就转移到了前端。这个时候前端的复杂性就迅速提升了。

实现 V1 的过程中,最大的挑战就是在触发跟踪事件时,需要记录上下文。这里的上下文是有相关的页面参数、相关的用户交互等定义的。

比如这么一个场景:我们正在收集用户在搜索结果页面中查看我们产品的曝光数据,然后,前端需要传递原始的用户在搜索栏中输入文本。

这个场景下,原始输入文本是跟这个曝光数据相关的,但是它并不直接存在产品卡片曝光事件中。

所以,为了支持这个场景下的数据,FE 开发人员需要付出很大的努力才能做到。

像这种存在于应用程序上下文中固有的东西,为了数据收集我们不得不明确地记录它们。

也就是因为这类的问题,所以才有了 V2 的诞生。

BI Tracking V2

那为什么 V2 能解决呢?

V2 基于两个公理:

  1. 任何单个 V1 埋点事件都可以基于用户的一个或多个交互时间线被重新创建。比如说,我要收集用户视频播放时长,我只需要两个点,一个开始播放的点,一个结束播放的点。
  2. 埋点数据都是一堆 UI/UX 相关数据的子集。任何的埋点数据都是用户交互下产生的,开发人员不需要去想哪些应该上报,只需要尽可能的把涉及到是用户交互数据上报即可。

然后,会有加一个中间层(MiddleLayer),作为连接前端 V2 Log 和 BI Tracking V1 的桥梁。 以此来达到既能节省开发人员时间,而且也不需要 BI Team 做大的修改。

理想情况,在这种模式下我们只需要上报一个个的点,然后在中间层去做各种数据的收集、整合和计算。

现有 RN 项目 V2 版本的实现

现有的 RN 项目同时存在 V1 和 V2 版本,但主体上是使用的 V2 版本,V1 版本的埋点会逐步废弃。

然后,RN 底层埋点逻辑是直接调用 Native Modules 提供的 GAShopeeBITrackerV2 模块里的方法来发送的。

主要有三个方法:

  1. trackActions:多条数据上报
  2. trackPerformanceEvent:性能数据上报
  3. trackActionsRealTime:实时数据上报

详细的 API 文档在这里:GAShopeeBITrackerV2

实际情况

这里,虽然,我们使用了 V2 版本,但是我们实际只是用到了里面的一部分东西:

新的埋点结构。 尽可能少的定义埋点。比如:不同页面可以有相同的埋点值,不需要每次都去定义。然后通过 pageName 做区分。

RN 层做的事情

在 RN 层要做的事情:

  1. 在业务代码中收集数据
  2. 通过 trackering schema 校验数据
  3. 通过 processorResolver 组装数据
  4. 最后调用 Native Modules 提供的方法发送。

整个流程中,最复杂的就是数据收集层面,也就是第一步做的事情。

我们现在几乎所有的数据收集处理代码都是跟着业务一起写的。最初业务不大,逻辑不复杂的时候,其实也是没有什么问题的。

但随着业务需求和埋点需求一步步变多,对应的我们也遇到了很大的问题和挑战。

问题

代码层面

最近也是做了几个埋点相关的需求,从代码层面能直观看到的一些问题:

一、埋点代码和业务代码藕合紧密

我们的业务代码已经非常庞大了,对应的业务需求也没有减弱的迹象,而对应的埋点代码和业务代码藕合得也是十分的紧密,越是核心的地方埋点就越多。

很多代码可以说都是在为埋点而做,既而导致整体的代码看着就非常庞大繁杂,非常难以维护。

举一个非常典型的例子:videoPlayer 的 HOCs,纯粹为埋点服务的就是 5 个 HOC,这还没有包括还有 2 个 HOC 中含有埋点代码。

可以说 videoPlayer 的绝大部分代码都是埋点。

当然,这个时候也是最需要我们能停下来好好想想如何去做到一个更好维护的代码。

二、埋点中存在很多的数据计算和 Context 数据

我们代码中有很多的埋点,不仅仅只是打一个点那么简单,还涉及到很多数据计算和逻辑处理也放到了前端代码里。

而且,还需要很多非直接但相关联的一些数据需要合并上报。

这也是一个从侧面能看出其中埋点的复杂性所在。

三、相似埋点

同一个地方因为诉求上的一些差异,上报了相似的埋点数据。比如:video_like 和 video_like_cancel(这个后面看主要是方便产品查数据)

四、老埋点

有些埋点,可能只是试验性的,某一个阶段在用,后面就不再用了。

但这些埋点依旧会一直存在代码里,并不会有清退一说。

线上事故

然后,最近一段时间,我们也是发生了几个比较大的线上埋点问题。

目前看到的并有记录的有:

  1. 211216 版本导致的埋点事故分析
  2. 220308版本videoPlayTime埋点时长突变的问题
  3. 220312 - 分辨率突变问题分析

当然,我们可以说有导致的原因很多,有代码规范,有 CR,有本身业务代码不熟悉。

但更重要的还是业务逻辑本身已经够复杂了,但依旧还要在业务逻辑里追加很多的埋点代码,既而导致整个系统复杂性进一步提高。

对于开发人员来说,每一次的开发和改动都是步步惊心。

对于测试人员来说,每一次的测试都可能不够完善。

因为你不知道你的改动会不会影响到其他地方。

改造

目标

把业务逻辑和埋点逻辑分离。

定理

在谈下面方案之前,必须定义一个定理,也是前提,就是:

所有相互关联的事件的触发都是有固定时序的。

如果相关联的事件本身时序都不能保证,那么你本身代码逻辑就是有问题。

比如:一个页面的 pageView 事情是优先页面上的其他事件的,一个组件的 mounted 事件一定是早于 unmouted 事件的。

埋点类型划分

从广义上来说,埋点分两种类型,一种是瞬时埋点,一种是持续埋点。

瞬时埋点

就仅仅记录一个动作/交互,比如:page_view、xx_click。

它本身并没有特别的逻辑,主要是本身需要携带很多的 Context Data(埋点额外附带的数据)。

除非有一些特别的 Context Data 不能够实时获取,否则一般都是即时发送。

持续埋点

持续埋点一般是一段时间周期内的统计,是由一个或多个动作/交互组合而成。比如:video_play_time。

它的结果会依赖于很多的动作、事件、交互,并通过计算得来。

而且很多时候本身依赖的 Context Data 也是不能够实时获取到的。

既而导致需要在很多地方处理。

正常情况下,瞬时埋点几乎不会出问题。出问题的,也是复杂度最高的就是持续埋点了。

而我们方案的重点就是要解决在持续埋点中遇到的种种问题。

详细方案设计

我们要知道,要彻底把业务逻辑和埋点逻辑分开,那必须需要用到事件模型,我们这里采用了订阅发布模式。

架构图

event-tracking

整体设计思路

整个的设计思路总体是和 BI Tracking V2 的思想非常契合的,就是做一个中间层来做处理和计算。

我们把所有可能的计算逻辑抽离出来,放到一个个埋点 plugin 中去做计算。

这也是为什么我们要定义一个定理的原因,因为 plugin 是强依赖相关联事件的时序的。

Events 模块

这样一来就有了 Events 模块。

我们把页面的切换,RN 的回调,各种动作和事件都做一个统一的收集、管理、分发。

同时,我们还需要保证一个事件的订阅者出现的异常不能影响到其他订阅着,所以我们需要一个异常处理模块来保证。

Plugins 模块

有一些埋点本身有很多逻辑是相通的,但是如果都放到一块的话,早期可能还好,但后期一旦逻辑发生变化就意味着各种逻辑判断和处理。

所以,为了后期的维护,以及清晰读懂一个埋点,我们把所有的埋点都当着一个个 Plugin 。

核心:一个埋点对应一个 plugin。

它本身包含了自身埋点所需要的各种逻辑处理。而这些逻辑处理所需要那些变动、回调、事件都可以通过监听 Events 来做对应的逻辑处理。

确实有通用数据处理逻辑,可以放到 Shared Data 模块或者单独抽离一个通用的 Util 去做。

Shared Data

Shared Data 的存在本身是因为我们很多个埋点本身依赖了很多相同的 Context Data。

如果每个埋点都自己做的话,就意味着大量重复的处理。比如常规的,我们需要保留上一页下一页数据,以及我们之后要做的 fromSource 参数要记录长达 5 级的结构,还有很多类似的参数。

等等这些都是需要有一个统一的地方去管理和维护。

所以,才有了这么一个公共的共享数据源,来管理和维护这些需要共享的单个数据点。

Processors

这里面 Processors 是现有的 Tracker 的实现,磊哥和林锐在这里已经完善得非常不错了,整体暂时保持不变。

额外收获

测试

业务代码和埋点代码分离后,我们可以看到整个埋点的处理都统一到了一个大的模块里。

外层只需要通过事件驱动的方式就可以让埋点代码跑起来。

而这个时候,整个的埋点测试其实是完全可以做到完全独立的、并且是全自动化的。

即插即用

因为采用了插件模式来编写,所以可以非常方便的对某个埋点进行修改、删除,而不影响到其他埋点。

当然,除此之外,还可以做整体埋点技术层面的性能分析,数据优化和统计。

最后

上面所说的埋点问题的解决,都仅仅只是聚焦在了现有的代码层面。

在我看来,真实能解决的东西其实不多,也就是降低了一些复杂性、更适合测试。

要想真正去解决,还需要更全方面的去考虑。比如把前端的计算逻辑移除放到 DA 中做,比如无埋点模式。