之前我们聊过了Sentry的异常监控方案中具体有那几种异常,以及大概的处理方式。这次我们来了解一下,这些异常数据的上报机制是怎么样的。
上报方式
就目前了解到的,主流的数据上报方式 而言,Sentry还是采用的ajax上报的方式。为了有更好的兼容性,在初始化的时候会去判断浏览器是否支持fetch,支持就使用fetch否则是xhr。同时也支持自定义的上报方式,且优先级会高于fetch和xhr
class BaseBackend {
if (this._options.transport) {
return new this._options.transport(transportOptions);
}
if (supportsFetch()) { return new FetchTransport(transportOptions); };
return new XHRTransport(transportOptions);
}
上报流程
以unhandledrejection为例,首先是 全局监听 触发对应的triggerHandlers
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中的 captureEvent
addInstrumentationHandler({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (e: any) => {
currentHub.captureEvent(event, {
originalException: error,
});
return;
},
type: 'unhandledrejection',
});
触发baseclient.ts 中的_captureEvent
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;
},
);
}
最后走到核心主流程的函数方法上_processEvent
核心方法_processEvent
baseclient.ts _processEvent 参数event代表sentry要发送的事件本身的信息(event_id,timestamp,release
等等),hint代表其他的一些和原始异常相关的信息(captureContext,data,originalException等等),scope代表元数据的作用域
// 代码有部分删减
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}`,
);
});
}
这一块的流程比较多,虽然已做删减,还是需要分成几个模块来讲解分析
前置条件
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%的数据会被发送,适用于日活非常大的情形。
添加通用配置信息
this._prepareEvent(event, scope, hint)
主要是添加每个事件都需要的通用信息 如environment,message,dist,release, breadcrumbs等等
数据上报前的处理函数
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源码(三)| 数据上报
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。