以前咱们聊过了Sentry的异常监控方案中具体有那几种异常,以及大概的处理方式。此次咱们来了解一下,这些异常数据的上报机制是怎么样的。javascript
就目前了解到的,主流的数据上报方式 而言,Sentry仍是采用的ajax上报的方式。为了有更好的兼容性,在初始化的时候会去判断浏览器是否支持fetch,支持就使用fetch不然是xhr。同时也支持自定义的上报方式,且优先级会高于fetch和xhrhtml
class BaseBackend { if (this._options.transport) { return new this._options.transport(transportOptions); } if (supportsFetch()) { return new FetchTransport(transportOptions); }; return new XHRTransport(transportOptions); }
以unhandledrejection为例,首先是 全局监听 触发对应的triggerHandlersjava
function instrumentUnhandledRejection(): void { _oldOnUnhandledRejectionHandler = global.onunhandledrejection; global.onunhandledrejection = function(e: any): boolean { triggerHandlers('unhandledrejection', e); if (_oldOnUnhandledRejectionHandler) { // eslint-disable-next-line prefer-rest-params return _oldOnUnhandledRejectionHandler.apply(this, arguments); } return true; }; }
对应的handler触发instrument.ts中的 captureEventgit
addInstrumentationHandler({ // eslint-disable-next-line @typescript-eslint/no-explicit-any callback: (e: any) => { currentHub.captureEvent(event, { originalException: error, }); return; }, type: 'unhandledrejection', });
触发baseclient.ts 中的_captureEventgithub
protected _captureEvent(event: Event, hint?: EventHint, scope?: Scope): PromiseLike<string | undefined> { return this._processEvent(event, hint, scope).then( finalEvent => { return finalEvent.event_id; }, reason => { logger.error(reason); return undefined; }, ); }
最后走到核心主流程的函数方法上_processEventajax
baseclient.ts _processEvent 参数event表明sentry要发送的事件自己的信息(event_id,timestamp,release
等等),hint表明其余的一些和原始异常相关的信息(captureContext,data,originalException等等),scope表明元数据的做用域typescript
// 代码有部分删减 protected _processEvent(event: Event, hint?: EventHint, scope?: Scope): PromiseLike<Event> { const { beforeSend, sampleRate } = this.getOptions(); if (!this._isEnabled()) { return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.')); } const isTransaction = event.type === 'transaction'; if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) { return SyncPromise.reject( new SentryError( `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, ), ); } return this._prepareEvent(event, scope, hint) .then(prepared => { const beforeSendResult = beforeSend(prepared, hint); if (isThenable(beforeSendResult)) { return (beforeSendResult as PromiseLike<Event | null>).then( event => event, e => { throw new SentryError(`beforeSend rejected with ${e}`); }, ); } return beforeSendResult; }) .then(processedEvent => { const session = scope && scope.getSession && scope.getSession(); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); } this._sendEvent(processedEvent); return processedEvent; }) .then(null, reason => { if (reason instanceof SentryError) { throw reason; } this.captureException(reason, { data: { __sentry__: true, }, originalException: reason as Error, }); throw new SentryError( `Event processing pipeline threw an error, original event will not be sent. Details have been sent as a new event.\nReason: ${reason}`, ); }); }
这一块的流程比较多,虽然已作删减,仍是须要分红几个模块来说解分析npm
if (!this._isEnabled()) { return SyncPromise.reject(new SentryError('SDK not enabled, will not send event.')); } const isTransaction = event.type === 'transaction'; if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) { return SyncPromise.reject( new SentryError( `Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`, ), ); }
前面基本是对是否知足上报的条件进行校验,初始化的时候是否设置了enabled = false(默认为true),为false即Sentry不可以使用,不会上报数据。设置的sampleRate采样率。好比设置了sampleRate = 0.1即会有10%的数据会被发送,适用于日活很是大的情形。json
this._prepareEvent(event, scope, hint)
主要是添加每一个事件都须要的通用信息 如environment,message,dist,release, breadcrumbs等等api
beforeSend其实就是Sentry.init传入的函数,入参即为event,hint,最后返回event。便于使用方对event数据作处理过滤,等等
const session = scope && scope.getSession && scope.getSession(); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); } this._sendEvent(processedEvent); return processedEvent;
判断是否有session,有则更新
_sendEvent则指向对应的transport(由于浏览器兼容fetch,则本次实际上报方式是使用fetch)
public sendEvent(event: Event): PromiseLike<Response> { return this._sendRequest(eventToSentryRequest(event, this._api), event); }
这里咱们看到,在上报前还会执行eventToSentryRequest,这个方法主要是在序列化参数
export function eventToSentryRequest(event: Event, api: API): SentryRequest { const req: SentryRequest = { body: JSON.stringify(sdkInfo ? enhanceEventWithSdkInfo(event, api.metadata.sdk) : event), type: eventType, url: useEnvelope ? api.getEnvelopeEndpointWithUrlEncodedAuth() : api.getStoreEndpointWithUrlEncodedAuth(), }; return req; }
Fetch中最后实现上报的地方为fetch.ts _sendRequest
private _sendRequest(sentryRequest: SentryRequest, originalPayload: Event | Session): PromiseLike<Response> { if (this._isRateLimited(sentryRequest.type)) { return Promise.reject({ event: originalPayload, type: sentryRequest.type, reason: `Transport locked till ${this._disabledUntil(sentryRequest.type)} due to too many requests.`, status: 429, }); } const options: RequestInit = { body: sentryRequest.body, method: 'POST', referrerPolicy: (supportsReferrerPolicy() ? 'origin' : '') as ReferrerPolicy, }; return this._buffer.add( new SyncPromise<Response>((resolve, reject) => { this._fetch(sentryRequest.url, options) .then(response => { const headers = { 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), 'retry-after': response.headers.get('Retry-After'), }; this._handleResponse({ requestType: sentryRequest.type, response, headers, resolve, reject, }); }) .catch(reject); }), ); }
咱们能够看到sentry中经过_isRateLimited方法来防止一瞬间太多相同的错误发生。
最终上报的数据格式为
{ "exception":{ "values":[ { "type":"UnhandledRejection", "value":"Non-Error promise rejection captured with value: 321", "mechanism":{ "handled":false, "type":"onunhandledrejection" } } ] }, "level":"error", "platform":"javascript", "event_id":"a94cd62ee6064321a340ce396da78de0", "timestamp":1617443534.168, "environment":"staging", "release":"1537345109360", "request":{ "url":"http://127.0.0.1:5500/packages/browser/examples/index.html", "headers":{ "Referer":"http://127.0.0.1:5500/packages/browser/examples/index.html", "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" } }, "sdk":{ "name":"sentry.javascript.browser", "version":"6.2.5", "integrations":[ ], "packages":[ { "name":"npm:@sentry/browser", "version":"6.2.5" } ] } }
其实这篇文档在写到一半的时候,我忽然意识到一个略显尴尬的问题,我好像没有具体写错误数据是如何处理的,就直接写了上报的流程。可是毕竟写都写了,前期仍是花了比较多的精力,从新开始就有点浪费时间了。因而我决定在后面的一篇中补充上,Sentry对于异常数据的处理。ps: 由于本身以前作过一次监控SDK,在对Sentry了解的越多后,感受到了本身以前的不少不足,同时也印证了本身以前的一些想法,这个系列不出意外应该还会持续下去。
GitHub - getsentry/sentry-javascript: Official Sentry SDKs for Javascript
解析Sentry源码(三)| 数据上报