cover_image

从零开始构建前端监控

陈华嗣 SQB Blog
2023年03月27日 03:30

编辑:陆家靖

图片

背景

公司内部有比较完善的服务端监控,但是对客户端的监控却很少。再加上使用的外部产品很融入到内部的整个监控系统体系之内。因此我们自研了一套前端(客户端)监控系统。它不仅能捕捉到错误,还能用记录数据还原整个错误场景,帮助用户快速定位和解决问题。对各种页面性能统计和分析,也能帮助用户提前发现问题,找到性能瓶颈。和其他的监控系统一样,前端监控也分为三部分,它们各司其职,又相辅相成:

  • 链路 Tracing
  • 指标 Metrics
  • 日志 Logging
图片

开端

设计一个监控系统,先从收集数据开始,那应该收集哪些数据呢?

  • 页面性能相关数据
  • 错误相关数据
  • 客户端数据

性能相关数据

Google凭借chrome浏览器的广泛使用,主导了web性能标准 Web指标[1],其中核心指标,侧重于用户体验的三个方面--加载性能、交互性和视觉稳定性。

  • Largest Contentful Paint (LCP) :最大内容绘制,测量加载性能,
  • First Input Delay (FID) :首次输入延迟,测量交互性,
  • Cumulative Layout Shift (CLS) :累积布局偏移,测量视觉稳定性。

当然还需要其他指标帮助诊断特殊问题。例如:Time to First Byte 首字节时间 (TTFB) 和 First Contentful Paint 首次内容绘制 (FCP) 指标都是加载体验的重要方面,并且在诊断LCP问题方面(分别为服务器响应时间过长或阻塞渲染资源)都十分有用。

错误数据

浏览器中错误主要分为两种:Error[2]DOMException[3]。最常见就是 Error,当代码运行的发出错误,会创建新的Error对象,并将其抛出。

按照捕获方式又可以分为以下几类:

  1. 脚本错误。可以使用 window.onerror,
  2. 资源加载错误。可以使用 object.onerror 和 performance 接口,
  3. Promise错误。可以监听 unhandledrejection 事件。

客户端数据

浏览器的相关信息和用户相关信息。其中有浏览器名称和版本、操作系统版本、webview 容器名称和版本、和网络等信息。此外还可以收集用户的名称和标识等相关信息。对这类数据的收集,主要还是用于帮助还原整个错误场景,更好的定位问题。因为涉及用户,这块数据尤其需要注意数据安全,依照最小可用原则来进行规范地收集信息。

整体设计

收集的数据都已经明确,那就开始整体设计。其中包括三个重要的环节:采集器、传输与存储和查询与分析。

图片

采集器

前端页面交互都是基于事件机制设计的,因此采集器就是一个事件监听器,可以监听 onerror 和 unhandledrejection 事件,一旦页面发生了错误,就可以捕获它,然后发送给后端服务器。整个流程很简单,就是监听与发送错误。

直接发送错误会有什么问题呢?

如果遇到了陷入了死循环的错误,一下子抛出很多一样的错误,然后发送网络请求,之后就会触发浏览器的 TCP 并发数限制,6个 TCP 连接数很快被占用,同时还会有大量的网络任务堆积,占用大量内存。那有什么好办法呢?

  1. 用数组建立一个简单的队列,进行流量控制,并设置数组的长度上限,
  2. 对一段时间内的相同错误去重,丢弃掉多余的错误信息。

现在收集到了错误数据,有错误类型,名称,还有错误堆栈。错误堆栈格式每个浏览器还不太一样,可以参考 Tracekit[4],但是在转换完错误堆栈格式会发现,这些数据都是类似 a,b,f 这样的压缩数据,很难真正地帮忙大家排查出问题。

都是压缩数据,看不懂那怎么办呢?

那不得不提 SourceMap[5],简单来说,SourceMap 就是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系。因此需要如下步骤:

  1. 打包时通过 webpack 插件上传 SourceMap 文件,
  2. 存储错误数据的时候,再根据 SourceMap 文件和报错堆栈还原出源文件的报错信息。

那错误数据收集完,该收集页面性能相关数据了。同样是监听页面事件,当页面加载的时候,在客户端计算好各个性能指标。页面加载的过程中还有很多事件,比如资源的加载、页面的渲染、网络请求、和交互事件等。这些都是属于 tracing 范畴的数据,可以做成 Timeline 时间线,更好地帮助用户发现整个时序内,什么时刻发生了什么操作,各个操作之间的父子关系等,在这时序内的一系列操作就相当于一个事务,所有需要发送整个事务。

那事务什么时候开始,什么结束呢?

可以从页面加载或者页面跳转开始启动事务,但是结束时间就很难确定了。比如:

使用 onload 作为结束事件,那至少有两个问题:

  1. 如果有个图片加载很慢,用户就直接跳转了,那这个事务并没有结束,就只能被抛弃了,
  2. 很多页面都是 SPA 单页面,页面的跳转是通过 history 实现的,并不会触发 onload事件。

那改成使用 beforeunload 事件,也会有问题:

  1. 用户加载了页面,然后就放着不管了,
  2. 用户浏览完页面,用户切换到其他应用,并没有关闭,那就一直触发不了 beforeunload 事件。

那是不是一定得用这基于事件的设计呢?

可以设计一个心跳检测的机制,最好让用户能根据自身业务灵活配置。事务开始开始后,启动心跳,期间会不断有事件触发,操作进入事务,当在两个心跳期间并没有任何新增事件,将会触发事务结束。心跳可以开放给用户配置。同时为了防止事务体积过大,还可以设置事务的最长有效时间。

图片

传输与存储

数据已经采集完毕,现在需要将数据发送到服务器,就改考虑如何传输了。

目前数据发送主要有这两种方式:

  1. img 请求上报,创建一个图片,在 URL 上带上参数。比如:img.src="http://www.google-analytics.com/__utm.gif?utmwv=4&utmn=769876874&..."
  2. Ajax 的 POST 方式,数据直接放在 body 里。

使用那种方式更合适呢?

图片 image 本质是一个 GET 请求,对上报数据量有一定的限制,不同浏览器标准不一样,一般为2~8kb。这在事务比较大的情况下,很容易超出,更适合简单数据的收集场景。如果数据量偶尔超出,可以考虑数据分割,但是这又增加了系统的复杂度。

Ajax 的 POST 方式,直接使用 JSON 数据发送,数据量没有了限制,但是发现每次会多发一个请求。因为使用了 CORS[6] 去解决跨域问题,那浏览器每次用 OPTIONS 方法对服务器发起一个预检请求。那如何避免这个预检请求呢?那就需要将这个请求改成 CORS 简单请求,设置 Content-Type: text/plain。

数据可以发送了,但是体积可以优化吗?

既然都需要转成 Text,那为什么要一定用 JSON 呢?完全可以选择性能更好,体积更小的通信协议。比如使用 Protobuf[7],客户端和服务端共同维护一份 proto 文件,也方便后期的升级管理。Proto 数据字段的具体设计需要根据采集的数据确定,其中需要注意的服务器接受的数据除了来自于浏览器,还可能是其他客户端,比如:APP。Proto 输出的二进制数据,正如上面提到的 CORS 问题,需要将二进制转成 text,比如使用 Base64,而 APP 并不会存在这个问题。

已经进入了传输的过程一定就安全了吗?比如:浏览器正使用 Ajax 的 POST 方式发送数据,但是就在这个时候页面跳转,刷新或者被关闭了。数据不是就直接丢失了。

发送数据的时候,页面关闭了怎么办?

提起页面关闭时候的网络请求问题,那是不是就准备要上 navigation.sendBeacon?其实还真没有必要,Ajax 的方式可以选用 fetch 做为数据的发送方法,fetch 接口有个选项:keepalive[8] 就是专门用来解决这个问题的。

最后数据存到哪里呢?

数据都是一条条记录,并没有多少对象间的关系,完全可以选择文档型数据库或者面向日志的数据库。比如,选用阿里云 SLS 作为数据存储和分析。如果考虑到接入的客户端比较多,请求量很大,可能还需要接入一个消息队列系统,比如:Kafka。

查询与分析

用户需要查看页面的性能,报错信息,Api 请求,客户端信息等等。如果存储的时候都是在一张表里,将很难进行多维分析。因此要设计成多个表,比如拆分成:transaction 事务表,exception 错误表,span 操作表等等。

  • 指标分析

    利用 SLS 强大的查询和分析能力,可以统计出每个 transaction 的指标,能及时发现性能有问题的页面。然后在众多的相似数据中找到具有代表性的一个事务,查看它的详情。

    图片
  • 事务分析

    查找出这个事务中的所有操作,并按照时间顺序进行整合,然后绘制出一个时间线。这个时间线里可以查看到各种操作,比如资源加载,渲染长任务,网络请求等等。比如:可以根据基准线查看1秒的页面状态,找出性能瓶颈。

    图片
  • 错误分析

    及时发现页面的报错,特别对跨端的 H5 页面很有帮助。在特定的浏览器或者webview 容器内的报错,能更快更好的还原现场,查看报错信息。

    图片

终章

至此,整个监控系统设计完毕,我们一起再来回顾一遍,看一下监控三要素。

  • 链路 Tracing:transaction 的整个事务的收集过程,集合了各种 span,满足了 tracing 的链路与关联,
  • 指标 Metrics: 页面指标里 web-vitals 的收集,正对应了指标的聚合与统计,
  • 日志 Logging: exception 的错误信息,客户端信息等的收集,这不就是用日志来还原现场。

监控系统梳理完毕,当然还有很多设计需要优化,很多细节问题还欠考虑。生产环境中建议基于一些成熟的库进行开发,比如我们就是基于 sentry 核心库进行的二次开发。

关于作者

陈华嗣,来自技术平台部

参考资料

[1]

Web指标: https://web.dev/i18n/zh/vitals/

[2]

Error: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Error

[3]

DOMException: https://developer.mozilla.org/zh-CN/docs/Web/API/DOMException

[4]

Tracekit: https://github.com/csnover/TraceKit

[5]

SourceMap: https://developer.chrome.com/blog/sourcemaps/

[6]

CORS: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

[7]

Protobuf: https://protobuf.dev/

[8]

keepalive: https://developer.mozilla.org/en-US/docs/Web/API/fetch#keepalive


继续滑动看下一个
SQB Blog
向上滑动看下一个