本文作者:cjinhuo,未经授权禁止转载。
背景
传统方式下一个前端项目发到正式环境后,所有报错信息只能通过用户使用时截图、口头描述发送到开发者,然后开发者来根据用户所描述的场景去模拟这个错误的产生,这效率肯定超级低,所以很多开源或收费的前端监控平台就应运而生,比如:
等等一些优秀的监控平台
国内常用的监控平台
sentry :从监控错误、错误统计图表、多重标签过滤和标签统计到触发告警,这一整套都很完善,团队项目需要充钱,而且数据量越大钱越贵
fundebug:除了监控错误,还可以录屏,也就是记录错误发生的前几秒用户的所有操作,压缩后的体积只有几十 KB,但操作略微繁琐
webfunny:也是含有监控错误的功能,可以支持千万级别日PV量,额外的亮点是可以远程调试、性能分析,也可以docker
私有化部署(免费),业务代码加密过
为什么不选择上面三个监控平台或者其他监控平台,为什么要自己搞?
- 首先
sentry
和fundebug
需要投入大量金钱来作为支持,而webfunny
虽是可以用docker
私有化部署,但由于其代码没有开源,二次开发受限 - 自己开发可以将公司所有的SDK统一成一个,包括但不限于:埋点平台SDK、性能监控SDK
监控平台的组成
整体流程
整体流程
从上图可以看出来,如果需要自研监控平台需要做三个部分:
- APP监控SDK:收集错误信息并上报
- server端:接收错误信息,处理数据并做持久化,而后根据告警规则通知对应的开发人员
- 可视化平台:从数据存储引擎拿出相关错误信息进行渲染,用于快速定位问题
监控SDK
整体代码架构
代码架构
整体代码架构使用发布-订阅设计模式以便后续迭代功能,处理逻辑基本都在HandleEvents
文件中,这样设计的好处是如果想穿插hook
或者迭代功能可以在处理事件回调多添加一个函数
HandleEvents
web错误信息收集
一般情况下都是通过重写js原生事件然后拿到错误信息,比如ajax请求
,通过重写xhr
、fetch
事件来截取接口信息,所以我们需要优先编写一个易于重写事件的函数来复用。
replaceOld
接口错误
所有的请求第三方库都是基于xhr
、fetch
二次封装的,所以只需要重写这两个事件就可以拿到所有的接口请求的信息,通过判断status
的值来判断当前接口是否是正常的。举个例子,重写xhr
的代码操作:
Xhr重写
上面除了拿去接口的信息之外还做一个操作:如果是SDK发送的接口,就不用收集该接口的信息。如果需要发布事件就调用triggerHandlers(EVENTTYPES.XHR, this.mito_xhr)
,类似的,fetch
也是用这种方式来重写。
关于接口跨域、超时的问题:这两种情况发生的时候,接口返回的响应体和响应头里面都是空的,status
等于0,所以很难区分两者,但是正常情况下,一般项目中都的请求都是复杂请求,所以在正式请求会先进行option
进行预请求,如果是跨域的话基本几十毫秒就会返回来,所以以此作为临界值来判断跨域与超时的问题(如果是接口不存在也会被判断成接口跨域)。
js代码错误&&资源错误
监听window
的error
事件
window.addEventListener('error',function(e){
// 拿到错误信息,发布事件:triggerHandlers
}, true)
- 资源错误
判断e.target.localName
是否有值,有的话就是资源错误,在handleErrors
中拿到信息:
handleError
- 代码错误
上面判断为false
时,代表是代码错误,在回调中可以拿到对应的错误代码文件、代码行数等等信息,然后通过source-map这个npm包
+sourceMap
文件进行解析,就可以还原出线上真实代码错误的位置。
监听unhandledrejection
当Promise
被 reject
且没有 reject
处理器的时候,会触发 unhandledrejection
事件
unhandledrejection监听
用户行为信息收集
单纯收集错误信息是可以提高错误定位的效率,但如果再配合上用户行为的话就锦上添花,定位错误的效率再上一层,如下图所示,可以清晰的看到用户做了哪些事:进了哪个页面 => 点击了哪个按钮 => 触发了哪个接口:
用户行为前端页面展示
dom事件信息
dom
事件获取包括很多:click
、input
、doubleClick
等等,一种直接在window上面监听click事件(注意第三个参数为true
):
window.addEventListener('click',function(e){
// 利用节流,以防事件触发过快
// 发布事件 triggerHandlers
}, true)
还有一种是通过重写window.addEventListener
的方式来截取开发者对dom的监听事件。
路由切换信息
在单页应用中有两种路由变换:hashchange、history
- history
当浏览器支持history
模式时,会被以下两个事件所影响:pushState
、replaceState
,且这两个事件不会触发onpopstate的回调,所以我们需要监听这个三个事件:
onpopstate重写
- hashchange
当浏览器只支持hashchange
时,就需要重写hashchange:
hashchange重写
console信息
正常情况下正式环境是不应该有console
的,那为什么要收集console
的信息?第一:非正常情况下,正式环境或预发环境也可能会有console
,第二:很多时候也可以把sdk
放入测试环境上面调试。所以最终还是决定收集console
信息,但是在初始化的时候的传参来告诉sdk
是否监听console
的信息收集。
console重写
框架层错误信息收集
Vue
vue2.6
官网提供了两个报错函数的回调:Vue.config.errorHandler
和Vue.config.warnHandler
vue错误信息收集
React
React16.13中提供了componentDidCatch钩子函数来回调错误信息,所以我们可以新建一个类ErrorBoundary
来继承React,然后然后声明componentDidCatch
钩子函数,可以拿到错误信息(目前没写react的错误收集,看官网文档简述,简易版应该是这样写的)。
react错误信息收集
自定义上报错误
上面收集的是web端的代码错误、接口报错和框架层面的报错等等,还有一种是业务错误信息:比如点击支付的时候,可能服务端接口返回200,但是响应体是错误信息,就需要手动上报这块的错误信息。既然要手动上报,SDK
就需要提供一个全局函数功能开发者调用:
import MITO from 'mitojs'
MITO.log({
info: '支付失败,余额不足',
tag: 'business'
})
Breadcrumb收集
在上面收集完错误信息的时候,都在最后追加一行breadcrumb.push(data)
,这样就可以保存用户的行为轨迹,默认情况设置20
长度,也可以在初始化时可配置,但是建议最高不要超过100
,因为如果信息过多,内存占用过大,对页面不太友好。
类型整合
在每个事件类型的回调的时候都将类型整合:比如用户点击、路由跳转都是属于用户行为,这样做的原因是让开发者更好过滤无用信息和精准定位到需要的信息。
用户行为类型整合
Error id生成
每个错误事件触发时都会有很多信息,我们需要尽量保证每个不同信息的错误生成的id不一样,这边采取的措施是先将每个错误的对象key按照一定规则递归排序,然后根据每个对象的值进行hashCode
,得到一串errorId
上报错误信息
当SDK拿到错误的所有信息时需要上报到服务端,有几种方式上报服务端
通过xhr上报
通过xhr
上报,如果设置成异步的时候,当用户跳转新页面或者关闭页面时就会丢失当前这个请求,如果设置成同步,又会让页面造成卡顿的现象
sentry
目前是通过xhr
发送的,不过它在发送前会推到它设置的一个请求缓冲区 _buffer
,以此来优化并发请求过多的问题。
Image的形式来发送请求
特点:
- 没有跨域问题、
- 发 GET 请求之后不需要获取和处理数据、
- 服务器也不需要发送数据、
- 不会携带当前域名 cookie、不会阻塞页面加载,影响用户的体验,只需 new Image 对象、
- 相比于 BMP/PNG 体积最小,可以节约 41% / 35% 的网络资源小
Navigator.sendBeacon
MDN:可用于通过HTTP将少量数据异步传输到Web服务器,统计和诊断代码通常要在 unload
或者 beforeunload
事件处理器中发起一个同步 XMLHttpRequest
来发送数据。同步的 XMLHttpRequest
迫使用户代理延迟卸载文档,并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力
特点:
- 发出的是异步请求,并且是
POST
请求 - 发出的请求,是放到的浏览器任务队列执行的,脱离了当前页面,所以不会阻塞当前页面的卸载和后面页面的加载过程,用户体验较好
- 只能判断出是否放入浏览器任务队列,不能判断是否发送成功
Beacon API
不提供相应的回调,因此后端返回最好省略response body
- 兼容性不是很友好
用户唯一标识
为了方便统计用户量,在每次上报的时候会带一个唯一标识符trackerId
,生成这个trackerId
的途径有两种:
- 如果你是用
ajax
上报的话,发现cookie
中没有带trackerId
这个字段,服务端生成并setCookie
设置到用户端的cookie
- 直接用SDK生成,在每次上报之前都判断
localstorage
是否存在trackerId
,有则随着错误信息一起发送,没有的话生成一个并设置到localstorage
总结
SDK小结
订阅事件 => 重写原生事件 => 触发原生事件(发布事件) => 拿到错误信息 => 提取有用的错误信息 => 上报服务端
关于开源
SDK开源:mitojs,下一篇会讲服务端的表结构设计思路、怎样在千万条数据中多重标签毫秒级查询错误事件以及更好的告警机制通知开发人员
感兴趣的小伙伴可以点个关注,后续好文不断!!!