跳到主要内容

认识

2024年04月24日
柏拉文
越努力,越幸运

一、认识


1.1 工作流

Web 监控是一个全链路的监控体系, 包括数据采集数据加工处理削峰限流数据上报数据聚合数据清洗数据入库数据分析数据挖掘以及根据分析结果进行针对性的调整、消息推送等。

  1. 数据采集: 包括页面性能数据异常数据用户行为资源数据个性化指标等数据采集的过程

  2. 数据加工处理: 包括数据格式化、丰富上下文信息

  3. 削峰限流: 频率限制

  4. 数据上报: 上报方式上报时机上报优化

  5. 数据聚合

  6. 数据清洗

  7. 数据入库

  8. 数据分析与挖掘: 性能数据分析、异常数据分析、用户行为数据分析

  9. 消息推送

1.2 SDK 设计

为支持多平台、可拓展、可插拔的特点,整体SDK的架构设计是内核+插件的插件式设计;每个 SDK 首先继承于平台无关的 Core 层代码。然后在自身SDK中,初始化内核实例和插件。同时采用发布-订阅 设计模式, 这样设计的好处是便于后续扩展与维护,如果想添加新的hook或事件,在该回调中添加对应的函数即可。

1.3 SDK 难点、痛点

  1. 对于性能监控: 指标选取、品质度量、瓶颈定位

  2. 请求/静态资源监控: 如何尽可能的对齐后端口径?

  3. 对于 JS 异常监控: 如何提高感知、定位、处理的效率?

  4. SDK 的设计、实现: 如何做到降低侵入?如何降低性能损耗?如何兼顾体积控制与灵活应用?

二、功能


前端监控 SDK 用于收集并上报: 代码报错、性能数据、页面录屏、用户行为、白屏检测等个性化指标数据

2.1 支持多种上报方式

支持多种上报方式: 默认使用web beacon,也支持图片打点、http 上报

2.2 支持错误上报去重

支持错误上报去重: 开启缓存队列,存储报错信息, 错误生成唯一的id,重复的代码错误只上报一次

2.3 支持项目的白屏检测

支持项目的白屏检测: 兼容有骨架屏、无骨架屏这两种情况

2.4 支持多种错误还原方式

支持多种错误还原方式: 定位源码、播放录屏、记录用户行为

三、问题


3.1 SDK 如何设计成多平台支持?

在前端监控的领域里,我们可能不仅仅只是监控一个web环境下的数据,包括 Nodejs、微信小程序、Electron 等各种其余的环境都是有监控的业务需求在的。那么我们就要思考一个点,我们的一个 SDK 项目,既然功能全,又要支持多平台,那么怎么设计这个 SDK 可以让它既支持多平台,但是在启用某个平台的时候不会引入无用的代码呢?我们可以通过插件化对代码进行组织, 具体如下:

  • Core 来管理 SDK 内与平台无关的一些代码,比如一些公共方法(生成mid方法、格式化)

  • 然后每个平台单独一个 SDK, 去继承 core 的类, SDK 内自己管理SDK特有的核心方法逻辑,比如上报、参数初始化

  • 最后就是 Plugins 插件,每个SDK都是由内核+插件组成的,我们将所有的插件功能,比如性能监控、错误监控都抽离成插件

这样子进行 SDK 的设计有很多好处:

  1. 每个平台分开打包,每个包的体积会大大缩小

  2. 代码的逻辑更加清晰自恰

最后打包上线时,我们通过修改 build 的脚本,对 packages 文件夹下的每个平台都单独打一个包,并且分开上传到 npm 平台

3.2 SDK 如何方便的进行业务拓展和定制?

业务功能总是会不断迭代的,SDK 也一样,所以说我们在设计SDK的时候就要考虑它的一个拓展性, 我们采用内核+插件的设计:

  1. 内核里是SDK内的公共逻辑或者基础逻辑;比如数据格式化和数据上报是底下插件都要用到的公共逻辑;而配置初始化是SDK运行的一个基础逻辑;

  2. 插件里是SDK的上层拓展业务,比如说监听js错误、监听promise错误,每一个小功能都是一个插件;

  3. 内核和插件一起组成了SDK实例 Instance,最后暴露给客户端使用

可拓展这个问题的答案已经很清晰了,我们需要拓展业务,只需要在内核的基础上,不断的往上叠加插件的数量就可以了。至于说定制化,插件里的功能,都是使用与否不影响整个SDK运行的,所以我们可以自由的让用户对插件里的功能进行定制化,决定哪个监控功能启用、哪个监控功能不启用等等....

3.3 SDK 在拓展新业务的时候,如何保证原有业务的正确性?

在上述的内核+插件设计下,我们开发新业务对原功能的影响基本上可以忽略不计,但是难免有意外,所以在 SDK 项目的层面上,需要有 单元测试 的来保证业务的稳定性; 我们可以引入单元测试,并对 每一个插件,每一个内核方法,都单独编写测试用例,在覆盖率达标的情况下,只要每次代码上传都测试通过,就可以保证原有业务的一个稳定性;

3.4 SDK 如何实现异常隔离以及上报?

首先,我们引入监控系统的原因之一就是为了避免页面产生错误,而如果因为监控SDK报错,导致整个应用主业务流程被中断,这是我们不能够接受的。

实际上,我们无法保证我们的 SDK 不出现错误,那么假如万一SDK本身报错了,我们就需要它不会去影响主业务流程的运行;最简单粗暴的方法就是把整个 SDK 都用 try catch 包裹起来,那么这样子即使出现了错误,也会被拦截在我们的 catch 里面。但是我们回过头来想一想,这样简单粗暴的包裹,会带来哪些问题:

  1. 我们只能获取到一个报错的信息,但是我们无法得知报错的位置、插件;

  2. 我们没有将其上报,我们无法感知到 SDK 产生了错误

  3. 我们没法获取 SDK 出错的一个环境数据

那么,我们就需要一个相对优雅的一个异常隔离+上报机制,回想我们上文的架构:内核+插件的形式;我们对每一个插件模块,都单独的用trycatch包裹起来,然后当抛出错误的时候,进行数据的封装、上报。这样子,就完成了一个异常隔离机制:

  1. 它实现了: 当SDK产生异常时不会影响主业务的流程

  2. SDK产生异常时进行数据的封装、上报

  3. 出现异常后,中止 SDK 的运行,并移除所有的监听

3.5 SDK 如何实现服务端时间的校对?

看到这里,可能有的同学并不明白,进行服务端时间的校对是什么意思;我们首先要明白,我们通过 JS 调用 new Date() 获取的时间,是我们的机器时间;也就是说:这个时间是一个随时都有可能不准确的时间;

那么既然时间是不准确的,假如有一个对时间精准度要求比较敏感的功能:比如说 API全链路监控;最后整体绘制出来的全链路图直接客户端的访问时间点变成了未来的时间点,直接时间穿梭那可不行;

如上图,我们先要了解的是,http响应头 上有一个字段 Date, 它的值是服务端发送资源时的服务器时间,我们可以在初始化SDK的时候,发送一个简单的请求给上报服务器,获取返回的 Date 值后计算 Diff 差值存在本地。

这样子就可以提供一个公共API,来提供一个时间校对的服务,让本地的时间比较趋近于服务端的真实时间(只是比较趋近的原因是:还会有一个单程传输耗时的误差

let diff = 0;
export const diffTime = (date: string) => {
const serverDate = new Date(date);
const inDiff = Date.now() - serverDate.getTime();
if (diff === 0 || diff > inDiff) {
diff = inDiff;
}
};

export const getTime = () => {
return new Date(Date.now() - diff);
};

当然,这里还可以做的更精确一点,我们可以让后端服务在返回的时候,带上 API 请求在后端服务执行完毕所消耗的时间 server-timing,放在响应头里;我们取到数据后,将 ttfb 耗时 减去返回的 server-timing 再除以 2;就是单程传输的耗时;那这样我们上文的计算中差的单程传输耗时的误差 就可以补上了

3.6 SDK 如何实现会话级别的错误上报去重?

首先,我们需要理清一个概念,我们可以认为:

  1. 在用户的一次会话中,如果产生了同一个错误,那么将这同一个错误上报多次是没有意义的;

  2. 在用户的不同会话中,如果产生了同一个错误,那么将不同会话中产生的错误进行上报是有意义的;

为什么有上面的结论呢?理由很简单:

  1. 在用户的同一次会话中,如果点击一个按钮出现了错误,那么再次点击同一个按钮,必定会出现同一个错误,而这出现的多次错误,影响的是同一个用户、同一次访问;所以将其全部上报是没有意义的

  2. 而在同一个用户的不同会话中,如果出现了同一个错误,那么这不同会话里的错误进行上报就显得有意义了

有一个生成错误 mid 的操作,这是一个唯一id,但是它的唯一规则是针对于不同错误的唯一

// 对每一个错误详情,生成一串编码
export const getErrorUid = (input: string) => {
return window.btoa(unescape(encodeURIComponent(input)));
};

所以说我们传入的参数,是 错误信息、错误行号、错误列号、错误文件等可能的关键信息的一个集合,这样保证了产生在同一个地方的错误,生成的 错误mid 都是相等的;这样子,我们才能在错误上报的入口函数里,做上报去重

3.7 SDK 如何生成错误唯一 ID

上面我们有提到一个 错误ID,它的作用分两种:

  1. 在客户端用以实现会话级别的上报去重

  2. 在服务端用以实现相同错误的数据聚合

但在实际应用中,我们如果仅仅根据错误信息、错误行号、错误列号、错误文件 来进行判断,可能还不够准确,所以,我们需要将堆栈信息纳入聚合算法中。我们根据错误堆栈,解析出了错误文件名、错误函数名、错误行号、错误列号等信息, 再利用上述的所有信息,最终生成一个 hash 值,这个值就是能够完全的描述这个错误的唯一性的 ID

3.8 SDK 采用什么样的上报策略?

请查看 上报 文章。

3.9 平台数据如何进行 削峰限流?

请查看 上报/削峰限流 文章。

3.10 平台数据为什么需要数据加工?

那么,为什么需要数据加工,以及数据加工需要做什么处理?

当我们的数据上报之后,因为IP地址 是在服务端获取的嘛,所以服务端就需要有一个服务,去统一给请求数据中家加上IP地址 以及IP地址 解析后的归属地、运营商等信息;

根据业务需要,还可以加上服务端服务的版本号 等其余信息,方便后续做追踪;

3.11 平台数据为什么需要数据清洗?

请前往上报/数据清洗

3.12 平台数据为什么需要数据聚合?

请前往上报/数据聚合

3.13 平台数据如何进行多维度数据追踪?

请前往上报/数据追踪

3.14 代码错误如何进行源码映射?

请前往采集/SourceMap

3.15 如何设计监控告警的维度?

请前往预警

3.16 为什么要自研前端监控体系?

参考资料


从0到1搭建前端监控平台,面试必备的亮点项目

腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?

使用 Sentry 做异常监控 - Sentry 是如何做到自动捕获前端应用异常的呢 ?

字节前端监控实践

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

解析Sentry源码(一)| 搞懂Sentry初始化