Sentry 的异样数据处理流程
获取异样
在 Sentry 的前端异样监控计划中之前咱们说过,Sentry 的全局异样获取形式有 2 种,window.onerror 以及 unhandledrejection。
异样数据的解决
以 unhandledrejection 为例 globalhandlers.ts 中
addInstrumentationHandler({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callback: (e: any) => {
let error = e;
// dig the object of the rejection out of known event types
try {
// PromiseRejectionEvents store the object of the rejection under 'reason'
// see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
if ('reason' in e) {error = e.reason;}
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
// https://github.com/getsentry/sentry-javascript/issues/2380
else if ('detail' in e && 'reason' in e.detail) {error = e.detail.reason;}
} catch (_oO) {// no-empty}
const currentHub = getCurrentHub();
const hasIntegration = currentHub.getIntegration(GlobalHandlers);
const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;
// addEventListener 的事件中间接 throw Error 的时候,shouldIgnoreOnError 会为 true. 即不会上报
// 起因是在 instrumentDOM 办法中,用 globalDOMEventHandler 办法对监听事件包了一层, 使得 ignoreOnError>0
if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {return true;}
const client = currentHub.getClient();
const event = isPrimitive(error)
? this._eventFromRejectionWithPrimitive(error)
: eventFromUnknownInput(error, undefined, {attachStacktrace: client && client.getOptions().attachStacktrace,
rejection: true,
});
event.level = Severity.Error;
addExceptionMechanism(event, {
handled: false,
type: 'onunhandledrejection',
});
currentHub.captureEvent(event, {originalException: error,});
return;
},
type: 'unhandledrejection',
});
整体流程分为以下几个模块解说
兼容解决
try {
// PromiseRejectionEvents store the object of the rejection under 'reason'
// see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
if ('reason' in e) {error = e.reason;}
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
// https://github.com/getsentry/sentry-javascript/issues/2380
else if ('detail' in e && 'reason' in e.detail) {error = e.detail.reason;}
} catch (_oO) {// no-empty}
这一块都是在做兼容性解决,适配不同浏览器中的谬误类型
过滤条件
const currentHub = getCurrentHub();
const hasIntegration = currentHub.getIntegration(GlobalHandlers);
const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;
if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {return true;}
这里的 hasIntergration 是在判断是否装置了对应的全局监听函数的插件(初始化的时候就会默认加载)
shouldIgnoreOnError : addEventListener 的事件中间接 throw Error 的时候,shouldIgnoreOnError 会为 true. 即不会上报
起因是在 instrumentDOM 办法中,用 globalDOMEventHandler 办法对监听事件包了一层, 使得 ignoreOnError>0
isFailedOwnDelivery 即判断是否为 Sentry 本身的申请谬误,若是,则不上报。
外围解决逻辑
const client = currentHub.getClient();
const event = isPrimitive(error)
? this._eventFromRejectionWithPrimitive(error)
: eventFromUnknownInput(error, undefined, {attachStacktrace: client && client.getOptions().attachStacktrace,
rejection: true,
});
export function isPrimitive(wat: any): wat is Primitive {return wat === null || (typeof wat !== 'object' && typeof wat !== 'function');
}
isPrimitive(error)即在判断 error 是否为原始数据类型
若为原始数据类型,则解决比较简单
private _eventFromRejectionWithPrimitive(reason: Primitive): Event {
return {
exception: {
values: [
{
type: 'UnhandledRejection',
// String() is needed because the Primitive type includes symbols (which can't be automatically stringified)
value: `Non-Error promise rejection captured with value: ${String(reason)}`,
},
],
},
};
}
若为援用数据类型,外围处理函数在 tracekit.ts 为
stack = computeStackTraceFromStacktraceProp(ex);
if (stack) {return popFrames(stack, popSize);
}
具体就在 computeStackTraceFromStacktraceProp,popFrames 这 2 个函数中。
function computeStackTraceFromStackProp(ex: any): StackTrace | null {if (!ex || !ex.stack) {return null;}
const stack = [];
const lines = ex.stack.split('\n');
let isEval;
let submatch;
let parts;
let element;
for (let i = 0; i < lines.length; ++i) {if ((parts = chrome.exec(lines[i]))) {const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
if (isEval && (submatch = chromeEval.exec(parts[2]))) {
// throw out eval line/column and use top-most line/column number
parts[2] = submatch[1]; // url
parts[3] = submatch[2]; // line
parts[4] = submatch[3]; // column
}
element = {
// working with the regexp above is super painful. it is quite a hack, but just stripping the `address at `
// prefix here seems like the quickest solution for now.
url: parts[2] && parts[2].indexOf('address at') === 0 ? parts[2].substr('address at'.length) : parts[2],
func: parts[1] || UNKNOWN_FUNCTION,
args: isNative ? [parts[2]] : [],
line: parts[3] ? +parts[3] : null,
column: parts[4] ? +parts[4] : null,
};
} else if ((parts = winjs.exec(lines[i]))) {
element = {url: parts[2],
func: parts[1] || UNKNOWN_FUNCTION,
args: [],
line: +parts[3],
column: parts[4] ? +parts[4] : null,
};
} else if ((parts = gecko.exec(lines[i]))) {isEval = parts[3] && parts[3].indexOf('> eval') > -1;
if (isEval && (submatch = geckoEval.exec(parts[3]))) {
// throw out eval line/column and use top-most line number
parts[1] = parts[1] || `eval`;
parts[3] = submatch[1];
parts[4] = submatch[2];
parts[5] = ''; // no column when eval
} else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) {
// FireFox uses this awesome columnNumber property for its top frame
// Also note, Firefox's column number is 0-based and everything else expects 1-based,
// so adding 1
// NOTE: this hack doesn't work if top-most frame is eval
stack[0].column = (ex.columnNumber as number) + 1;
}
element = {url: parts[3],
func: parts[1] || UNKNOWN_FUNCTION,
args: parts[2] ? parts[2].split(',') : [],
line: parts[4] ? +parts[4] : null,
column: parts[5] ? +parts[5] : null,
};
} else {continue;}
if (!element.func && element.line) {element.func = UNKNOWN_FUNCTION;}
stack.push(element);
}
if (!stack.length) {return null;}
return {message: extractMessage(ex),
name: ex.name,
stack,
};
}
能够看出,外围流程其实就是在对不同的浏览器做兼容解决,以及将数据组合成堆栈的模式
后续
将异样数据进行捕捉以及解决之后,就是上报流程了。详情能够参看上一篇文档 Sentry 的异样数据上报机制
currentHub.captureEvent(event, {originalException: error,});
总结
在大抵理解到了整个异样数据处理流程后,发现重点是在对于不同浏览器的兼容性解决上,整个链路比拟清晰,两头很多繁琐的解决细节也都没有太多的去关注,毕竟工夫和精力绝对无限,想的是在对 Sentry 的整体脉络有了肯定理解之后,对本人做监控能有很多启发,当然这也是钻研 Sentry 源码的初衷。