从Sentry源码架构看前端监控实现(一)整体架构

2,376 阅读9分钟

从Sentry源码架构看前端监控实现(一)整体架构

本文主要从源码架构上对Sentry的实现进行拆解,Sentry的基本接入配置参考官网或其他文章

前端异常和性能监控是前端工程化必备知识。Sentry作为主流异常和性能监控平台,支持多语言多平台,在前端领域也支持React, Vue, Next等多框架。相信从Sentry架构和实现的学习中能掌握到一款监控平台需要覆盖哪些功能,监控哪些异常情况,又该如何实现异常的监控捕获,除了基本的异常信息外又该上报哪些额外信息帮助用户问题排查

整体架构

image.png

Sentry中主要有HubClientIntegration三大部分组件:

  • Hub:可以理解为中心管理系统,事件集线器,上报到Sentry服务的事件一般会统一经由Hub组件处理派发。从外部讲,用户调用API会经由Hub处理,从内部讲,各事件的捕获也会由Hub进行处理
  • Client:实际处理事件并进行上报的客户端实例,实现了对事件的捕获到发送的全流程
  • Integration:集成,也可以理解为插件,Client实例可以绑定多个Integration,具体的事件监听和回调处理由不同的Integration实现

addInstrumentationHandler并非组件实例,而是底层的API函数,Integration中主要依赖该函数进行事件的注册,所以作为底层依赖体现在架构图中

Sentry整体架构分层实现上基本如上图所示,由下到上,由instrument检测浏览器事件,触发Integration层面绑定的事件回调函数,然后上报给Hub中心,再转交给Client客户端实例,最后由客户端实例对捕获到的事件进行处理发送

image.png

Hub

Hub作为事件中心处理器,会在Sentry.init()初始化时自动创建并提供开箱即用的API,init()创建的Hub实例会作为单例对象挂载在Window上,同时会初始化BrowserClient实例绑定在Hub实例上。可以通过window.__SENTRY__.hub获取到Hub实例(当然Sentry也提供了对外APIgetCurrentHub()获取当前Hub实例),通过hub.getClient()获取到BrowserClient客户端实例

Sentry.init()

image.png

大部分框架应用在接收到配置参数进行初始化时,首先第一步是对配置参数进行校验和标准化,Sentry同样会首先进行标准化处理,然后通过getCurrentHub()(此API同样暴露给用户)获取到当前Hub实例或者创建新的Hub实例,getCurrentHub()内会自动判断当前是否存在Hub实例,当不存在时自动创建实例,同时会将实例挂载在Window上。接下来Sentry会创建BrowserClient实例并通过hub.bindClient(client)挂载在Hub实例上,这一步存在其他操作,会在下文讲述。此时初始化完成,用户可以通过getCurrentHub()获取当前Hub实例,可以通过getCurrentHub().getClient()获取客户端实例

可以总结出,Sentry初始化的过程也就是初始化HubClient客户端的过程。init()帮助用户自动完成了这一过程,如果存在特殊需求场景,用户当然也可以选择手动自行创建Client实例和Hub实例

Hub作为中心角色

Hub提供了事件捕获API。当用户直接调用Sentry暴露出来的captureEvent等API时,会自动获取当前的Hub实例对象(通过getCurrentHub()),并调用其对应的事件捕获函数。Hub内部会将事件信息与当前Scope数据相结合一并交由Client进行处理

内部Integration实现上,也是通过getCurrentHub().captureEvent()API将事件捕获的任务交由Hub进行处理

image.png

可以得出结论,无论从外部用户侧API还是内部Integration侧,事件捕获都会统一经由Hub处理,所以可以将Hub视为中心化事件总线

Scope管理

Hub作为事件捕获中心处理器,如果只是对Client客户端事件捕获API的封装那么意义并不大。其实Hub内另外维护了一个重要对象Scope,标签,用户信息等其实是挂载在Scope中,可以理解为作用域或者上下文的形式。ScopeHub中以栈的形式维护,推入新的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

image.png

安装Integration也就是遍历整个Integration数组,逐一调用integration.setupOnce()进行初始化操作。所以在init()后整个Sentry系统的初始化操作(包括HubClientIntegration)完成,此时已经建立了Sentry和浏览器系统事件的关联,对应浏览器事件触发时将会被Sentry捕获到

事件处理流程

image.png

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对事件进行处理,这里的处理过程类似中间件形式,依次调用处理器函数处理事件,并将处理后的事件结果作为下一项处理器的事件参数

image.png

范围事件处理器可以通过操作Scope来添加注册,全局事件处理器可以通过暴露的APIaddGlobalEventProcessor()添加。Sentry系统内默认Integration会添加全局事件处理器

  • 发送前处理

Sentry系统内对事件的处理在_prepareEvent已经全部处理完毕,发送前处理是预留给用户侧进行处理的阶段。用户可以配置beforeSendbeforeSendTransaction两个钩子函数分别处理ErrorEventTransactionEvent并返回处理结果

  • 发送事件

事件的发送由客户端的transport进行处理,默认使用fetch或者XMLHttpRquest发送请求,用户也可以通过options.transport指定自定义请求。另外发送事件也会在当前Scope添加面包屑记录

Integration

HubClient两个上层组件主要负责事件捕获后的处理,Integration则负责和底层浏览器事件或者浏览器操作打交道。上面提到了Integration的两个主要工作:一是通过addInstrumentationHandler向浏览器绑定事件回调函数,二是通过addGlobalEventProcessor向Sentry系统内注册全局事件处理器。另外不同的Integration分别负责不同模块的工作,所以Integration集成的角色类似于客户端插件

向下addInstrumentationHandler将浏览器事件抽象为七大类型的检测机制:consoleDOMxhrfetchhistoryerrorunhandledrejection,大部分是先是通过对浏览器API进行重写的方法注入回调函数进行检测

image.png

  • consolefetch是对函数进行重写实现绑定回调
  • xhr主要重写了XMLHttpRequest.prototypeopensendreadystatechange
  • history覆盖了onpopstate事件回调,重写了pushStatereplaceState操作
  • DOM检测对document添加了clickkeypress事件监听,并重写了EventTarget.prototypeNode.prototypeaddEventListenerremoveEventListener在元素触发clickkeypress时触发相关操作
  • errorunhandledrejection是对window.onerrorwindow.onunhandledrejection进行覆盖重写

默认集成

Sentry提供了一些开箱即用的默认Integration,主要包括以下部分

  • InboundFilters:默认空配置,不做处理。使用该集成允许用户根据给定异常中的类型、消息或 URL 忽略特定错误。
  • FunctionToString:不注册事件处理器,也不检测浏览器事件,重写Function.prototype.toString实现范会Sentry封装后的函数的原函数名
  • TryCatch:该集成使用try/catch封装异步API和事件API(setTimeoutsetIntervalrequestAnimationFrameaddEventListener/removeEventListenerXMLHttpRequest事件回调)以处理并上报异步错误,会额外添加argumentsmechanism等信息
  • Breadcrumbs:通过addInstrumentationHandler封装全部Sentry所支持的检测API以收集面包屑
  • GlobalHandlers:通过addInstrumentationHandler检测浏览器errorunhandledrejection事件并提交到Hub进行捕获,这两个事件是最基本的错误捕获类型
  • LinkedErrors:允许用户处理错误链,通过事件处理器的形式,递归访问Exceptioncause字段并以数组形式挂载在Event
  • Dedupe:通过全局事件处理器的形式删除某些事件的重复上报。通过比较堆栈跟踪和指纹,拦截连续的重复事件
  • HttpContext:此集成将UA、referrer,和HTTP请求信息(如 URL、Header)附加到事件。允许用户使用特定的操作系统、浏览器和版本信息正确地对事件进行分类和标记

默认的这些集成已经能捕获异常并提供面包屑,浏览器信息等。GlobalHandler实现了最基本的异常捕获,HttpContext提供了浏览器用户代理等信息,Breadcrumbs记录了请求,DOM操作,console,浏览器历史记录变化等页面状态的变更,TryCatch能够帮助在异步函数中添加参数信息

性能监控

Sentry默认集成侧重点在于错误捕获上报,并没有涉及到性能监控。实现性能监控需要额外添加BrowserTracing集成到配置中

Sentry.init({
  integrations: [new BrowserTracing()],
  // ...
});
复制代码

BrowserTracing主要通过addInstrumentationHandler追踪fetchxhrhistory事件,记录每次操作的状态和时间跨度(如fetch请求的成功失败状态,请求耗时,浏览器切换地址耗时)等信息,以Transaction对象保存在Scope

创建Transaction时记录开始时间,transaction.finish()结束时记录结束时间,以此表示一项操作的时间跨度,当finish()时,会同捕获事件一般通过hub.captureEvent(transaction)处理并上报Transaction记录

浏览器性能监控必不可少的需要测量相关重要指标。BrowserTracing在实例化时即绑定相关事件,实现追踪记录CLSLCPFID指标。指标数据以Measurements结构保存在Transaction数据中,每次上报Transaction时一同上报到Sentry