目前的项目中很早前就引入了Sentry进行错误监控,自己以前也零零散散看过一些错误监控的博客,但都没有深入到源码层面。很好奇Sentry是怎么抓取错误的,能够获取到那么多的错误信息。于是花了几天的闲暇时间加上周末,把它的源码撸了一遍,虽然没有仔细阅读每一行代码,但还是学到了很多,这篇文章就是用于记录自己的学习笔记。注意,这里不会介绍如何安装Sentry,因为网上的教程很多。
配置及主入口 install 方法
配置很简单,几行代码就可以完成:
| 1 | import Raven from 'raven-js'; | 
可能很好奇为啥是Raven而不是Sentry,这个我也不知道,不去深究。。。
可以看到一个config+addPlugin+install就 ok 了,addPlugin是用于安装Raven专门给Vue写的一个插件,install就是整个Raven的主入口了。
addPlugin很简单,其实就是把参数放到自己内部的一个插件数组中,等待合适时机安装每个插件:
| 1 | addPlugin: function(plugin /*arg1, arg2, ... argN*/) { | 
我们的RavenVue老哥其实也很简单,就是实现了Vue.config.errorHandler,这是 Vue 的全局错误处理钩子, 然后把抓到的错误交给Raven处理:
| 1 | // vuePlugin就是RavenVue | 
可以看到主要逻辑就是捕捉到错误时先获取vue实例或组件的一些信息,最后发送到服务器。至于captureException是怎么实现的我们后面再看。
接下来就是我们的重点install了:
| 1 | install: function() { | 
我们不去管各种 if 条件,无非是判断各种配置,先假设他们全部成立。那么我们的注意力就很清晰了:
- TraceKit.report.subscribe
- _handleOnErrorStackInfo
- _attachPromiseRejectionHandler
- _patchFunctionToString
- _instrumentTryCatch
- _instrumentBreadcrumbs
- _drainPlugins
余下所有篇幅都是解析他们的实现过程,挨个来看。
TraceKit.report.subscribe
TraceKit是个啥呢?在源码中有大段注释,摘来看一下:
| 1 | /* | 
它是一个跨浏览器的错误堆栈处理库,因为堆栈信息在不同浏览器上的细节都不尽相同,Raven把Trackit的相关代码专门放到一个文件了。
TraceKit.report有关于堆栈更详细的注释,对于理解代码很有帮助:
| 1 | /** | 
类似Raven的plugins数组,TraceKit.report也有一个handlers数组,我们的TraceKit.report.subscribe就是把自己注册到这个数组里:
| 1 | TraceKit.report = (function reportModuleWrapper() { | 
installGlobalHandler会拦截window.onrror,对错误对象进行一系列处理,最后交由每个handler处理。
| 1 | function installGlobalHandler() { | 
把上面的traceKitWindowOnError逻辑略去了很多,只看关键的TraceKit.computeStackTrace和notifyHandlers。 前者的逻辑很多,是用来帮助调用方屏蔽跨浏览器的堆栈处理细节,后者很简单,就是挨个调用每个handler处理stack信息。
我们先看简单的notifyHandlers:
| 1 | /** | 
可以看到确实很简单。再来看看复杂的TraceKit.computeStackTrace。
TraceKit.computeStackTrace
这个函数首先给了一大坨注释用于描述跨浏览器堆栈信息的混乱,此函数的目的就是返回一个统一格式的堆栈信息。
| 1 | /** | 
上面的注释大家了解一下就好,接下来看看具体的代码:
| 1 | TraceKit.computeStackTrace = (function computeStackTraceWrapper() { | 
整个TraceKit.computeStackTrace会挨个尝试用不同方法来解析堆栈,先尝试适用于 Chrome 的套路,不行再尝试适用于 Safari 或 IE 的,如果还是不行就手动构造。我们的重点放在适用于 Chrome 的computeStackTraceFromStackProp。
computeStackTraceFromStackProp
说实话这个函数的实现没有怎么看,涉及到奇怪的很多正则,整个函数的代码很多,大致的逻辑是先把错误堆栈信息按换行符切割,然后针对每一行提取各种信息:
- url错误的文件 url
- func错误的函数
- args调用的参数
- line行
- column列
最后如果url是以blob:开头的,会尝试通过相关链接拉取真正的url:
| 1 | // NOTE: blob urls are now supposed to always have an origin, therefore it's format | 
最后不管怎么样,我们会拿到一个『归一化』的堆栈信息,最后交由了handler处理,接下来看看我们注册的handler是如何处理的。
_handleOnErrorStackInfo
我们的handler就是这个函数,跟踪这个函数会发现真正调用的是_handleStackInfo函数:
| 1 | _handleStackInfo: function(stackInfo, options) { | 
_processException内部经过一系列处理,最终会调用_send方法,_send会把处理后的信息发送给server,发送时会先尝试使用fetch api,不兼容的话再使用XHR,此处代码不难。
_send中一个重要的步骤就是携带上_breadcrumbs数组,这个数组记录了用户的行为轨迹,后面会说到轨迹是在什么时候被记录的。
可能会好奇,server的 url 怎么拿到的呢?是不是就是在config函数传入的那个 url 呢?好吧其实并不是,这个 url 需要经过一些处理才能拿到。
首先发送的 url 是存储在全局的_globalEndpoint中,他是在setDSN方法中被赋值:
| 1 | setDSN: function(dsn) { | 
参数中的DSN才是我们传给config的值,如果我们的dsn为http://123456@sentry.io.com/5,那么最终的_globalEndpoint就是http://sentry.io.com/api/5/store。
好,小结一下,到目前为止我们知道Raven通过TraceKit这个库监听了window.onerror事件,并由TraceKit处理了复杂的错误信息并最终获得归一化的堆栈信息,然后Raven会拿着这个信息再经过一些处理最后发送给 server 端,发送的地址是由我们传入的配置决定的。
_attachPromiseRejectionHandler
捕捉未catch的Promise错误,然后会调用captrueException处理异常。这里会先调用Tracekit.computeStackTrace处理堆栈信息,然后调用_handleStackInfo.
| 1 | /** | 
我们来看看captureException,他是用于手动发送错误到server。
captureException
| 1 | 
 | 
代码注释很详细了,可以看到captureException有两种出口:
- captureMessage: 与_handleStackInfo 的过程类似,会手动发送一条信息给 server
- _handleStackInfo: 前面已经介绍了,会经过一系列处理后把错误发送给后端
_patchFunctionToString
用于将函数转为字符串:
| 1 | _patchFunctionToString: function() { | 
这里有个__raven__变量,如果有这个属性说明此函数是一个wrapped function,其__orig__表示原始函数。__raven__属性会在原函数被作用于wrap函数时赋值,wrap函数是一个很重要的函数,后面会说到。
_instrumentTryCatch
这个函数用于包裹各种异步回调,例如setTimeout,将回调函数wrap住,wrap内部会使用try-catch包裹原始函数调用,并在出错时将信息发送给server。
| 1 | 
 | 
上面代码主要做了两件事:
- 利用fill+wrap函数拦截了setTimeout、setInterval、requestAnimationFrame的实现,将其中的回调函数使用try-catch包裹,回调出错时使用captureException发送
- 使用类似的技巧,内部也利用了wrapEventTarget函数拦截各种对象上的addEventListener
fill和wrap定义如下:
| 1 | /** | 
大体来说,fill将obj上的name属性值替换成replacement,并使用track记录原始对应关系。
| 1 | /* | 
wrapped会替换真正的原始回调,使用try-catch包裹原始函数调用,这样就能捕获错误了,整个过程我们的业务代码都是无感知的。
小结一下_instrumentTryCatch做的事情:
- 利用fill+wrap函数拦截了setTimeout、setInterval、requestAnimationFrame的实现,将其中的回调函数使用try-catch包裹,回调出错时使用captureException发送
- 使用类似的技巧,内部也利用了wrapEventTarget函数拦截各种对象上的addEventListener。
_instrumentBreadcrumbs
用于记录行为轨迹,包括路由切换、控制台日志、xhr/fetch 请求、点击事件等,将轨迹放到全局_breadcrumbs数组中,之后发送 server 时会携带。
| 1 | 
 | 
里面有几个辅助函数,这里稍作说明,每个辅助函数的代码都比较好理解。
- captureBreadcrumb: 将参数对象放到全局的- _breadcrumbs数组中,在此之前如果用户设置了- breadcrumbCallback,会先把参数给此- callback处理一下。- _breadcrumbs会在- _send中被发送到后端,- _send会由- _processException或- captureMessage调用,其中- _processException会由- _handleStackInfo调用.所以可以认为- captureBreadcrumb中的- _breadcrumbs会在之后向后台发送错误时携带上.
- _breadcrumbEventHandler:记录发生- dom事件时,事件目标节点的在- dom tree中的路径(如果重复触发多次则只记录第一次)。路径只记录从当前节点到最高 5 级父节点,最终格式为- ....grandparent>parent>node. 获取的路径最后也是由- captureBreadcrumb处理.
- _keypressEventHandler: 主要是记录- input/textarea上的- keypress事件,- _breadcrumbEventHandler更多的是针对鼠标事件。- keypress事件通过- 1000ms的- debounce做了截流,最终还是会调用- _breadcrumbEventHandler记录路径.
- _captureUrlChange:对之前和现在的- url进行一些处理后,交由- captureBreadcrumb处理.
如果理解了_instrumentTryCatch的套路,那么理解_instrumentBreadcrumbs就会简单很多,因为他们都是通过fill+wrap的组合来做到这些。
小结一下_instrumentTryCatch做的事情:
- 拦截popstate、pushState、replaceState,利用_captureUrlChange函数记录当前url
- 拦截console上的debug,info,warn,error,log,利用captureBreadcrumb记录调用参数
- 拦截xhr,在open时利用__raven_xhr变量记录发送的url+method,在send时- 使用wrap包裹onload,onerror,onprogress的回调,使用try-catch抓错
- 在onreadystatechange时,使用captureBreadcrumb记录本次请求的url+method+status_code
 
- 使用
- 拦截fetch,使用与拦截xhr类似的技巧,在fetch成功或失败时记录本次请求
- 拦截冒泡到document上的click、keypress事件,使用_breadcrumbEventHandler、_keypressEventHandler处理
_drainPlugins
跟前面的几个比起来,这个算很简单的了,就是拿到内部插件数组中的每个插件安装一下:
| 1 | _drainPlugins: function() { | 
总结
Raven通过各种方法来捕获错误,同时记录行为轨迹,所有的方式列举如下:
- TraceKit:监听全局- window.error事件,处理错误堆栈信息后发送给- Sentry Server
- _attachPromiseRejectionHandler:捕捉未- catch的- Promise错误,处理后发送- server
- _breadcrumbEventHandler:记录发生- dom事件时目标节点的路径,并在下次发送- server时携带
- _keypressEventHandler:记录- input/textarea上的- keypress事件,- 1000ms截流,最终调用- _breadcrumbEventHandler
- captureMessage、- captureException:手动发送错误到- server
- _instrumentTryCatch:- 拦截了setTimeout、setInterval、requestAnimationFrame的实现,将其中的回调函数使用try-catch包裹,回调出错时使用captureException发送
- 拦截Window等多个对象上的addEventListener,同样使用上面的方式包裹
 
- 拦截了
- _instrumentBreadcrumbs,记录各种操作,存为『面包屑』,并在下次发送- server时携带- 拦截 - popstate、- pushState、- replaceState,利用记录当前- url
- 拦截 - console上的- debug,- info,- warn,- error,- log,记录调用方法和参数
- 拦截 - xhr,在- open时记录发送的- url+method,在- send时- 包裹onload,onerror,onprogress的回调,使用try-catch抓错
- 在onreadystatechange时,记录本次请求的url+method+status_code
 
- 包裹
- 拦截 - fetch,使用与拦截- xhr类似的技巧,在- fetch成功或失败时记录本次请求
- 拦截冒泡到 - document上的- click、keypress事件,使用- _breadcrumbEventHandler、- _keypressEventHandler处理
 
- RavenVuePlugin: 设置- Vue.config.errorHandler,并将错误交由- captureException处理