目前的项目中很早前就引入了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
错误的文件 urlfunc
错误的函数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
处理