比较下内部埋点 SDK 与字节前端监控 SDK
惯例先上链接——字节的前端监控 SDK 是怎样设计的
比较异同
不同/不如点
字节 | 内部 |
---|---|
监控,代表着不仅仅是埋点,包括了众多类型的信息上报 | 埋点,只针对于页面一些特定行为的信息上报 |
环境多样( Web 应用、小程序、Electron 应用、跨端应用等等) | 仅针对 Web 应用,针对 Vue 做了特性提供 |
逻辑解耦,分独立模块 | 逻辑一体化,采集、上报都在内部 |
实现插件化,支持业务自行拓展 | 不支持拓展、插件化 |
上报量大,QPS 高,批量上报 | 上报量一般,基于神策 SDK 只支持单条上报 |
勉强能跟得上的点
- 通过错误处理,保证减少对业务的影响
- 写好单测,确保自身稳定性
来学其精华
逻辑解耦
前端环境众多,所以前端监控不能局限在浏览器环境,需要同时解决其他环境的监控需求。不同环境之间差异巨大,从提供的配置项,到监控的功能、上报的方式都会不一样。因此设计之初的目标应该就是同一套设计组装成不同的 SDK,第一步就是要逻辑解耦。
多环境下差异很大,但是主要做的事情是一样的:配置、采集数据、组装数据、上报数据。字节的 SDK 拆分成了以下几个模块:
Monitor
收集器,主动或被动地采集特定环境下的原始数据,组装为平台无关事件。
Builder
组装器,负责将收集器上报的平台无关事件转换为特定平台的上报格式。
Sender
发送器,负责发送逻辑,比如批量,重试等功能。
字节 SDK 都是 batchSender,批量发送,这样可以减少请求数,但是需要考虑一些发送的边缘情况,比如页面关闭前需要通过 sendBeacon
将未发送的事件都发送出去。
Sender 这块,学习到了一个新的 API:Navigator.sendBeacon() - Web API 接口参考 | MDN (mozilla.org)
ConfigManager
配置管理器,负责配置逻辑,比如合并初始配置和用户配置、拉取远端配置等功能。
Client
实例主体,负责串联配置管理器、收集器、组装器和发送器,串通整个流程,同时提供生命周期监听以供扩展 SDK 功能。
角色之间足够抽象,互相独立、各司其职。比如 Monitor 只负责收集,并不知道最终上报的具体格式;Builder 只做组装,组装完成后交给实例主体 Client ,由 Client 交给 Sender ;Sender 不知道收到的具体事件格式,只负责完成发送。
生命周期
监控做的事情就像一条单纯的流水线:初始化 => 采集数据 => 组装数据 => 上报数据,我们希望能在不同阶段执行各种操作,但又不希望直接将逻辑耦合在代码,这样不利于后期的迭代维护,也会导致体积一步步增加,走向重构的必然结果。
可以参考的一个思路是,抽象出来一条规范的生命周期,所有的功能借助生命周期的监听来实现,解决了体积不断膨胀的问题,也让 SDK 易于拓展。
生命周期钩子也分为两类:
- 回调类:只执行回调,不影响流程继续执行。
- 处理类:执行并返回修改后的有效值,如果返回无效值,将不再往下执行,终止上报。
保证原有业务的正确性
接入监控 SDK 的目的是为了发现问题,如果监控 SDK 的问题导致业务受到了影响,不免本末倒置。因此保证原有业务的正确性远远比监控本身更重要。
SDK 要将对业务有影响的敏感代码用 try catch 包裹起来,保证出了错误也不影响业务,比如 hook 类的操作, hook XHR 和 Fetch 等等。这个操作要胆大心细,同时 try catch 的范围能小则小。
其次是监控 SDK 自身的错误。我们也会将 SDK 自身的 关键代码 包裹 try catch ,确保一个错误不会影响整个监控流程。单纯的 try catch 将错误吞掉解决不了问题,这些错误可能导致某些监控数据没有收集完全,影响监控的完整性。因此 SDK 实现了一个 ObserveSelfErrorPlugin ,用于收集 SDK 自身的错误并上报。
如何减少对业务的影响
字节的 SDK 隐含着对监控 SDK 最基本的要求:不能带来性能问题。而我们自己的场景下往往还没到这种程度,因此可以学习一下大厂的思维。
字节 SDK 不能影响业务的首屏渲染,因此将插件分成两类,一类需要立即监听的就先加载,例如路由变化等的监听,这种延后了会导致数据遗漏;另一类不需要立即监听的就延后加载,例如静态资源性能监控这样晚点也没所谓的,延后也没问题。
同时字节 SDK 本身会有性能评估,单个插件的执行耗时多少,带来的副作用的耗时又是多少,这些都是需要评估的点。他们内部有基于开源的 Benchmark 性能测试来保证 SDK 自身的性能,这一块有需要到也可以深入研究。
如何尽早开始监听
监听不遗漏的前提是事件发生在开始监控之后。有一些超高优先级的事件,比如 JS 错误,发生的时机可能非常靠前,等不到监控脚本加载完成。字节 SDK 提供的方案是针对 script 接入的方式提供一个简短的脚本,内联在页面中。作用就是提前开始监听,保证高优的事件不被遗漏。
另外它还有一个作用:缓存调用命令。
监控脚本是异步加载的,因此会先挂载一个空函数,确保调用不报错;同时把对实例主体 Client 的调用命令缓存下来,记录下调用的时间和页面地址,确保能正确组装数据;等到监控脚本加载完成时再顺序执行,以此确保调用不遗漏。示例如下:
window[globalName] = function (m) {
const onceArguments = [].slice.call(arguments)
onceArguments.push(Date.now(), location.href)
window[globalName].precolletArguments.push(onceArguments)
}
window[globalName].precolletArguments = []
当然如果使用 npm 包接入的话,依然会有预收集的逻辑,因为 npm 包不会挂全局变量,所以逻辑稍微有一些不同,同时受限于引入的顺序,执行的时机会稍晚一些。