本文主要从源码架构上对Sentry的实现进行拆解,Sentry的基本接入配置参考官网或其他文章
前端异常和性能监控是前端工程化必备知识。Sentry作为主流异常和性能监控平台,支持多语言多平台,在前端领域也支持React, Vue, Next等多框架。相信从Sentry架构和实现的学习中能掌握到一款监控平台需要覆盖哪些功能,监控哪些异常情况,又该如何实现异常的监控捕获,除了基本的异常信息外又该上报哪些额外信息帮助用户问题排查
整体架构
Sentry中主要有Hub
, Client
, Integration
三大部分组件:
Hub
:可以理解为中心管理系统,事件集线器,上报到Sentry服务的事件一般会统一经由Hub
组件处理派发。从外部讲,用户调用API会经由Hub
处理,从内部讲,各事件的捕获也会由Hub
进行处理Client
:实际处理事件并进行上报的客户端实例,实现了对事件的捕获到发送的全流程Integration
:集成,也可以理解为插件,Client
实例可以绑定多个Integration
,具体的事件监听和回调处理由不同的Integration
实现
addInstrumentationHandler
并非组件实例,而是底层的API函数,Integration
中主要依赖该函数进行事件的注册,所以作为底层依赖体现在架构图中
Sentry整体架构分层实现上基本如上图所示,由下到上,由instrument
检测浏览器事件,触发Integration
层面绑定的事件回调函数,然后上报给Hub
中心,再转交给Client
客户端实例,最后由客户端实例对捕获到的事件进行处理发送
Hub
Hub
作为事件中心处理器,会在Sentry.init()
初始化时自动创建并提供开箱即用的API,init()
创建的Hub
实例会作为单例对象挂载在Window
上,同时会初始化BrowserClient
实例绑定在Hub
实例上。可以通过window.__SENTRY__.hub
获取到Hub
实例(当然Sentry也提供了对外APIgetCurrentHub()
获取当前Hub
实例),通过hub.getClient()
获取到BrowserClient
客户端实例
Sentry.init()
大部分框架应用在接收到配置参数进行初始化时,首先第一步是对配置参数进行校验和标准化,Sentry同样会首先进行标准化处理,然后通过getCurrentHub()
(此API同样暴露给用户)获取到当前Hub
实例或者创建新的Hub
实例,getCurrentHub()
内会自动判断当前是否存在Hub
实例,当不存在时自动创建实例,同时会将实例挂载在Window
上。接下来Sentry会创建BrowserClient
实例并通过hub.bindClient(client)
挂载在Hub
实例上,这一步存在其他操作,会在下文讲述。此时初始化完成,用户可以通过getCurrentHub()
获取当前Hub
实例,可以通过getCurrentHub().getClient()
获取客户端实例
可以总结出,Sentry初始化的过程也就是初始化Hub
和Client
客户端的过程。init()
帮助用户自动完成了这一过程,如果存在特殊需求场景,用户当然也可以选择手动自行创建Client
实例和Hub
实例
Hub作为中心角色
Hub
提供了事件捕获API。当用户直接调用Sentry暴露出来的captureEvent
等API时,会自动获取当前的Hub
实例对象(通过getCurrentHub()
),并调用其对应的事件捕获函数。Hub
内部会将事件信息与当前Scope
数据相结合一并交由Client
进行处理
内部Integration
实现上,也是通过getCurrentHub().captureEvent()
API将事件捕获的任务交由Hub
进行处理
可以得出结论,无论从外部用户侧API还是内部Integration
侧,事件捕获都会统一经由Hub
处理,所以可以将Hub
视为中心化事件总线
Scope管理
Hub
作为事件捕获中心处理器,如果只是对Client
客户端事件捕获API的封装那么意义并不大。其实Hub
内另外维护了一个重要对象Scope
,标签,用户信息等其实是挂载在Scope
中,可以理解为作用域或者上下文的形式。Scope
在Hub
中以栈的形式维护,推入新的Scope
会复制原有的Scope
,也就是上下文信息的继承
用户使用Sentry.setUser()
,Sentry.setTag()
时其实也是配置Hub
当前栈顶的Scope
Client
挂载到Hub
Client
组件作为事件处理的核心组件,实现了事件从捕获到上报的完整处理流程。Integration
组件实例也作为客户端集成的角色由Client
进行维护。上文提到,Client
对象在Sentry.init()
时实例化,并由hub.bindClient(client)
绑定到Hub
实例上。hub.bindClient(client)
内有一项重要操作就是:调用client.setupIntegrations()
安装Integration
安装Integration
也就是遍历整个Integration
数组,逐一调用integration.setupOnce()
进行初始化操作。所以在init()
后整个Sentry系统的初始化操作(包括Hub
, Client
, Integration
)完成,此时已经建立了Sentry和浏览器系统事件的关联,对应浏览器事件触发时将会被Sentry捕获到
事件处理流程
Client
对事件的操作主要有标准化事件,准备事件,发送前处理,发送事件几个步骤
- 标准化事件
标准化事件是当由入口captureMessage()
/ captureException()
捕获事件时,分别通过eventFromMessage
/ eventFromException
统一为Event
接口,再通过内部函数_captureEvent
统一处理,当使用captureEvent
入口时则不需要额外处理,可以直接调用_captureEvent
_captureEvent
处理过程参考右侧分层图。自上到下为函数调用栈,主要分为准备事件,发送前处理,发送事件三个步骤
- 准备事件
_prepareEvent
是为事件Event
对象添加时间戳,id,附加文件,集成列表等字段。其中一项重要的操作是scope.applyToEvent(event)
。上文提到,Hub
把事件转交给客户端进行处理时会额外携带当前Scope
数据,scope.applyToEvent(event)
就是将事件携带的Scope
对象的相关字段复制到Event
数据结构中,并调用_notifyEventProcessors
使用全局事件处理器getGlobalEventProcessors()
和范围事件处理器scope._eventProcessors
对事件进行处理,这里的处理过程类似中间件形式,依次调用处理器函数处理事件,并将处理后的事件结果作为下一项处理器的事件参数
范围事件处理器可以通过操作Scope
来添加注册,全局事件处理器可以通过暴露的APIaddGlobalEventProcessor()
添加。Sentry系统内默认Integration
会添加全局事件处理器
- 发送前处理
Sentry系统内对事件的处理在_prepareEvent
已经全部处理完毕,发送前处理是预留给用户侧进行处理的阶段。用户可以配置beforeSend
和beforeSendTransaction
两个钩子函数分别处理ErrorEvent
和TransactionEvent
并返回处理结果
- 发送事件
事件的发送由客户端的transport
进行处理,默认使用fetch
或者XMLHttpRquest
发送请求,用户也可以通过options.transport
指定自定义请求。另外发送事件也会在当前Scope
添加面包屑记录
Integration
Hub
和Client
两个上层组件主要负责事件捕获后的处理,Integration
则负责和底层浏览器事件或者浏览器操作打交道。上面提到了Integration
的两个主要工作:一是通过addInstrumentationHandler
向浏览器绑定事件回调函数,二是通过addGlobalEventProcessor
向Sentry系统内注册全局事件处理器。另外不同的Integration
分别负责不同模块的工作,所以Integration
集成的角色类似于客户端插件
向下addInstrumentationHandler
将浏览器事件抽象为七大类型的检测机制:console
, DOM
, xhr
, fetch
, history
, error
, unhandledrejection
,大部分是先是通过对浏览器API进行重写的方法注入回调函数进行检测
console
,fetch
是对函数进行重写实现绑定回调xhr
主要重写了XMLHttpRequest.prototype
的open
,send
,readystatechange
history
覆盖了onpopstate
事件回调,重写了pushState
,replaceState
操作DOM
检测对document
添加了click
,keypress
事件监听,并重写了EventTarget.prototype
和Node.prototype
的addEventListener
,removeEventListener
在元素触发click
,keypress
时触发相关操作error
和unhandledrejection
是对window.onerror
和window.onunhandledrejection
进行覆盖重写
默认集成
Sentry提供了一些开箱即用的默认Integration
,主要包括以下部分
InboundFilters
:默认空配置,不做处理。使用该集成允许用户根据给定异常中的类型、消息或 URL 忽略特定错误。FunctionToString
:不注册事件处理器,也不检测浏览器事件,重写Function.prototype.toString
实现范会Sentry封装后的函数的原函数名TryCatch
:该集成使用try/catch
封装异步API和事件API(setTimeout
,setInterval
,requestAnimationFrame
,addEventListener/removeEventListener
,XMLHttpRequest
事件回调)以处理并上报异步错误,会额外添加arguments
,mechanism
等信息Breadcrumbs
:通过addInstrumentationHandler
封装全部Sentry所支持的检测API以收集面包屑GlobalHandlers
:通过addInstrumentationHandler
检测浏览器error
和unhandledrejection
事件并提交到Hub
进行捕获,这两个事件是最基本的错误捕获类型LinkedErrors
:允许用户处理错误链,通过事件处理器的形式,递归访问Exception
的cause
字段并以数组形式挂载在Event
上Dedupe
:通过全局事件处理器的形式删除某些事件的重复上报。通过比较堆栈跟踪和指纹,拦截连续的重复事件HttpContext
:此集成将UA、referrer,和HTTP请求信息(如 URL、Header)附加到事件。允许用户使用特定的操作系统、浏览器和版本信息正确地对事件进行分类和标记
默认的这些集成已经能捕获异常并提供面包屑,浏览器信息等。GlobalHandler
实现了最基本的异常捕获,HttpContext
提供了浏览器用户代理等信息,Breadcrumbs
记录了请求,DOM操作,console,浏览器历史记录变化等页面状态的变更,TryCatch
能够帮助在异步函数中添加参数信息
性能监控
Sentry默认集成侧重点在于错误捕获上报,并没有涉及到性能监控。实现性能监控需要额外添加BrowserTracing
集成到配置中
Sentry.init({
integrations: [new BrowserTracing()],
// ...
});
复制代码
BrowserTracing
主要通过addInstrumentationHandler
追踪fetch
, xhr
, history
事件,记录每次操作的状态和时间跨度(如fetch
请求的成功失败状态,请求耗时,浏览器切换地址耗时)等信息,以Transaction
对象保存在Scope
中
创建Transaction
时记录开始时间,transaction.finish()
结束时记录结束时间,以此表示一项操作的时间跨度,当finish()
时,会同捕获事件一般通过hub.captureEvent(transaction)
处理并上报Transaction
记录
浏览器性能监控必不可少的需要测量相关重要指标。BrowserTracing
在实例化时即绑定相关事件,实现追踪记录CLS
, LCP
, FID
指标。指标数据以Measurements
结构保存在Transaction
数据中,每次上报Transaction
时一同上报到Sentry