阅读 13540

前端监控平台系列:JS SDK(已开源)

本文作者:cjinhuo,未经授权禁止转载。

背景

传统方式下一个前端项目发到正式环境后,所有报错信息只能通过用户使用时截图、口头描述发送到开发者,然后开发者来根据用户所描述的场景去模拟这个错误的产生,这效率肯定超级低,所以很多开源或收费的前端监控平台就应运而生,比如:

等等一些优秀的监控平台

国内常用的监控平台

sentry :从监控错误、错误统计图表、多重标签过滤和标签统计到触发告警,这一整套都很完善,团队项目需要充钱,而且数据量越大钱越贵

fundebug:除了监控错误,还可以录屏,也就是记录错误发生的前几秒用户的所有操作,压缩后的体积只有几十 KB,但操作略微繁琐

webfunny:也是含有监控错误的功能,可以支持千万级别日PV量,额外的亮点是可以远程调试、性能分析,也可以docker私有化部署(免费),业务代码加密过

为什么不选择上面三个监控平台或者其他监控平台,为什么要自己搞?

  1. 首先sentryfundebug需要投入大量金钱来作为支持,而webfunny虽是可以用docker私有化部署,但由于其代码没有开源,二次开发受限

  2. 自己开发可以将公司所有的SDK统一成一个,包括但不限于:埋点平台SDK、性能监控SDK

监控平台的组成

整体流程

整体流程

从上图可以看出来,如果需要自研监控平台需要做三个部分:

  1. APP监控SDK:收集错误信息并上报
  2. server端:接收错误信息,处理数据并做持久化,而后根据告警规则通知对应的开发人员
  3. 可视化平台:从数据存储引擎拿出相关错误信息进行渲染,用于快速定位问题

监控SDK

整体代码架构

flow

代码架构

整体代码架构使用发布-订阅设计模式以便后续迭代功能,处理逻辑基本都在HandleEvents文件中,这样设计的好处是如果想穿插hook或者迭代功能可以在处理事件回调多添加一个函数

handlerEvent

HandleEvents

web错误信息收集

一般情况下都是通过重写js原生事件然后拿到错误信息,比如ajax请求,通过重写xhrfetch事件来截取接口信息,所以我们需要优先编写一个易于重写事件的函数来复用。

replaceOld

replaceOld

接口错误

所有的请求第三方库都是基于xhrfetch二次封装的,所以只需要重写这两个事件就可以拿到所有的接口请求的信息,通过判断status的值来判断当前接口是否是正常的。举个例子,重写xhr的代码操作:

xhrReplace

Xhr重写

上面除了拿去接口的信息之外还做一个操作:如果是SDK发送的接口,就不用收集该接口的信息。如果需要发布事件就调用triggerHandlers(EVENTTYPES.XHR, this.mito_xhr),类似的,fetch也是用这种方式来重写。

关于接口跨域、超时的问题:这两种情况发生的时候,接口返回的响应体和响应头里面都是空的,status等于0,所以很难区分两者,但是正常情况下,一般项目中都的请求都是复杂请求,所以在正式请求会先进行option进行预请求,如果是跨域的话基本几十毫秒就会返回来,所以以此作为临界值来判断跨域与超时的问题(如果是接口不存在也会被判断成接口跨域)。

js代码错误&&资源错误

监听windowerror事件

window.addEventListener('error',function(e){
  // 拿到错误信息,发布事件:triggerHandlers
}, true)
复制代码
  • 资源错误

判断e.target.localName是否有值,有的话就是资源错误,在handleErrors中拿到信息:

handleError

handleError
  • 代码错误

上面判断为false时,代表是代码错误,在回调中可以拿到对应的错误代码文件、代码行数等等信息,然后通过source-map这个npm包+sourceMap文件进行解析,就可以还原出线上真实代码错误的位置。

监听unhandledrejection

Promisereject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件

replaceUnhandlerejecttion

unhandledrejection监听

用户行为信息收集

单纯收集错误信息是可以提高错误定位的效率,但如果再配合上用户行为的话就锦上添花,定位错误的效率再上一层,如下图所示,可以清晰的看到用户做了哪些事:进了哪个页面 => 点击了哪个按钮 => 触发了哪个接口:

breadcrumb

用户行为前端页面展示

dom事件信息

dom事件获取包括很多:clickinputdoubleClick等等,一种直接在window上面监听click事件(注意第三个参数为true):

window.addEventListener('click',function(e){
	// 利用节流,以防事件触发过快
  // 发布事件 triggerHandlers
}, true)
复制代码

还有一种是通过重写window.addEventListener的方式来截取开发者对dom的监听事件。

路由切换信息

在单页应用中有两种路由变换:hashchangehistory

  • history

当浏览器支持history模式时,会被以下两个事件所影响:pushStatereplaceState,且这两个事件不会触发onpopstate的回调,所以我们需要监听这个三个事件:

onpopstate

onpopstate重写
  • hashchange

当浏览器只支持hashchange时,就需要重写hashchange:

hashchange

hashchange重写

console信息

正常情况下正式环境是不应该有console的,那为什么要收集console的信息?第一:非正常情况下,正式环境或预发环境也可能会有console,第二:很多时候也可以把sdk放入测试环境上面调试。所以最终还是决定收集console信息,但是在初始化的时候的传参来告诉sdk是否监听console的信息收集。

relaceConsole

console重写

框架层错误信息收集

Vue

vue2.6官网提供了两个报错函数的回调:Vue.config.errorHandlerVue.config.warnHandler

errorHandle

vue错误信息收集

React

React16.13中提供了componentDidCatch钩子函数来回调错误信息,所以我们可以新建一个类ErrorBoundary来继承React,然后然后声明componentDidCatch钩子函数,可以拿到错误信息(目前没写react的错误收集,看官网文档简述,简易版应该是这样写的)。

react-errro

react错误信息收集

自定义上报错误

上面收集的是web端的代码错误、接口报错和框架层面的报错等等,还有一种是业务错误信息:比如点击支付的时候,可能服务端接口返回200,但是响应体是错误信息,就需要手动上报这块的错误信息。既然要手动上报,SDK就需要提供一个全局函数功能开发者调用:

import MITO from 'mitojs'
MITO.log({
  info: '支付失败,余额不足',
  tag: 'business'
})
复制代码

Breadcrumb收集

在上面收集完错误信息的时候,都在最后追加一行breadcrumb.push(data),这样就可以保存用户的行为轨迹,默认情况设置20长度,也可以在初始化时可配置,但是建议最高不要超过100,因为如果信息过多,内存占用过大,对页面不太友好。

类型整合

在每个事件类型的回调的时候都将类型整合:比如用户点击、路由跳转都是属于用户行为,这样做的原因是让开发者更好过滤无用信息和精准定位到需要的信息。

breadcrumb-category

用户行为类型整合

Error id生成

每个错误事件触发时都会有很多信息,我们需要尽量保证每个不同信息的错误生成的id不一样,这边采取的措施是先将每个错误的对象key按照一定规则递归排序,然后根据每个对象的值进行hashCode,得到一串errorId

上报错误信息

当SDK拿到错误的所有信息时需要上报到服务端,有几种方式上报服务端

通过xhr上报

通过xhr上报,如果设置成异步的时候,当用户跳转新页面或者关闭页面时就会丢失当前这个请求,如果设置成同步,又会让页面造成卡顿的现象

sentry目前是通过xhr发送的,不过它在发送前会推到它设置的一个请求缓冲区 _buffer,以此来优化并发请求过多的问题。

Image的形式来发送请求

特点:

  1. 没有跨域问题、
  2. 发 GET 请求之后不需要获取和处理数据、
  3. 服务器也不需要发送数据、
  4. 不会携带当前域名 cookie、不会阻塞页面加载,影响用户的体验,只需 new Image 对象、
  5. 相比于 BMP/PNG 体积最小,可以节约 41% / 35% 的网络资源小

Navigator.sendBeacon

**MDN:**可用于通过HTTP将少量数据异步传输到Web服务器,统计和诊断代码通常要在 unload 或者 beforeunload 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力

特点:

  1. 发出的是异步请求,并且是POST请求
  2. 发出的请求,是放到的浏览器任务队列执行的,脱离了当前页面,所以不会阻塞当前页面的卸载和后面页面的加载过程,用户体验较好
  3. 只能判断出是否放入浏览器任务队列,不能判断是否发送成功
  4. Beacon API不提供相应的回调,因此后端返回最好省略response body
  5. 兼容性不是很友好

用户唯一标识

为了方便统计用户量,在每次上报的时候会带一个唯一标识符trackerId,生成这个trackerId的途径有两种:

  1. 如果你是用ajax上报的话,发现cookie中没有带trackerId这个字段,服务端生成并setCookie设置到用户端的cookie
  2. 直接用SDK生成,在每次上报之前都判断localstorage是否存在trackerId,有则随着错误信息一起发送,没有的话生成一个并设置到localstorage

总结

SDK小结

订阅事件 => 重写原生事件 => 触发原生事件(发布事件) => 拿到错误信息 => 提取有用的错误信息 => 上报服务端

关于开源

SDK开源:mitojs,下一篇会讲服务端的表结构设计思路、怎样在千万条数据中多重标签毫秒级查询错误事件以及更好的告警机制通知开发人员

感兴趣的小伙伴可以点个关注,后续好文不断!!!