跳到主要内容

认识

2024年04月25日
柏拉文
越努力,越幸运

一、认识


异常监控的核心作用就是通过上报的异常,帮开发人员及时发现线上问题并快速修复。要达到这个目的,异常监控需要做到以下 3 点:

  1. 线上应用出现异常时,可以及时推送给开发人员,安排相关人员去处理。

  2. 上报的异常,含有异常类型、发生异常的源文件及行列信息、异常的追踪栈信息等详细信息,可以帮助开发人员快速定位问题。

  3. 可以获取发生异常的用户行为,帮助开发人员、测试人员重现问题和测试回归。

这三点,分别对应异常自动推送异常详情获取用户行为获取

Sentry 异常监控原理是如何做的呢?

  1. 为了能自动捕获应用异常,Sentry 劫持覆写了 window.onerrorwindow.unhandledrejection 这两个 api

  2. Sentry 内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括: dom 节点事件回调、setTimeout / setInterval 回调、xhr 接口调用、requestAnimationFrame 回调等

二、劫持、重写异常捕获API


为了能自动捕获应用异常,Sentry 劫持覆写了 window.onerrorwindow.unhandledrejection 这两个 api

2.1 window.onerror

为了能自动捕获应用异常,Sentry 劫持覆写了 window.onerror, 重写逻辑如下所示:

const oldErrorHandler = window.onerror;
window.onerror = function (msg, url, line, column, error) {
// 收集异常信息并上报
triggerHandlers('error', {
column: column,
error: error,
line: line,
msg: msg,
url: url,
});
if (oldErrorHandler) {
return oldErrorHandler.apply(this, arguments);
}
return false;
};

2.2 window.onunhandledrejection

为了能自动捕获应用异常,Sentry 劫持覆写了 window.onunhandledrejection, 重写逻辑如下所示:

const oldOnUnhandledRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function (e) {
// 收集异常信息并上报
triggerHandlers('unhandledrejection', e);
if (oldOnUnhandledRejectionHandler) {
return oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return true;
};

三、劫持、标记异常上下文


Sentry 内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括: dom 节点事件回调、setTimeout / setInterval 回调、xhr 接口调用、requestAnimationFrame 回调等

3.1 setTimeout

标记 setTimeout, 为了标记 setTimeout 类型的异常,Sentry 劫持覆写了原生的 setTimout 方法。新的 setTimeout 方法调用时,会使用 try ... catch 语句块包裹 callback

重写逻辑如下

const originSetTimeout = window.setTimeout;
window.setTimeout = function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var originalCallback = args[0];
// wrap$1 会对 setTimeout 的入参 callback 使用 try...catch 进行包装
// 并在 catch 中上报异常
args[0] = wrap$1(originalCallback, {
mechanism: {
data: { function: getFunctionName(original) },
handled: true,
// 异常的上下文是 setTimeout
type: 'setTimeout',
},
});
return original.apply(this, args);
}

callback 内部发生异常时,会被 catch 捕获,捕获的异常会标记 setTimeout

3.2 setInterval

标记 setInterval, 为了标记 setInterval 类型的异常,Sentry 劫持覆写了原生的 setInterval 方法。新的 setInterval 方法调用时,会使用 try ... catch 语句块包裹 callback

3.3 requestAnimationFrame

标记 requestAnimationFrame, 为了标记 requestAnimationFrame 类型的异常,Sentry 劫持覆写了原生的 requestAnimationFrame 方法。新的 requestAnimationFrame 方法调用时,会使用 try ... catch 语句块包裹 callback

3.4 dom event handler

标记 dom 事件 handler , 所有的 dom 节点都继承自 window.Node 对象,dom 对象的 addEventListener 方法来自 Nodeprototype 对象。为了标记 dom 事件 handlerSentryNode.prototype.addEventListener 进行了劫持覆写。新的 addEventListener 方法调用时,同样会使用 try ... catch 语句块包裹传入的 handler

重写逻辑如下

function xxx() {
var proto = window.Node.prototype;
...
// 覆写 addEventListener 方法
fill(proto, 'addEventListener', function (original) {

return function (eventName, fn, options) {
try {
if (typeof fn.handleEvent === 'function') {
// 使用 try...catch 包括 handle
fn.handleEvent = wrap$1(fn.handleEvent.bind(fn), {
mechanism: {
data: {
function: 'handleEvent',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
});
}
}
catch (err) {}
return original.apply(this, [
eventName,
wrap$1(fn, {
mechanism: {
data: {
function: 'addEventListener',
handler: getFunctionName(fn),
target: target,
},
handled: true,
type: 'instrument',
},
}),
options,
]);
};
});
}

handler 内部发生异常时,会被 catch 捕获,捕获的异常会被标记 handleEvent, 并携带 event nameevent target 等信息。

3.5 xhr 接口回调

标记 xhr 接口回调, 为了标记 xhr 接口回调,Sentry 先对 XMLHttpRequest.prototype.send 方法劫持覆写, 等 xhr 实例使用覆写以后的 send 方法时,再对 xhr 对象的 onloadonerroronprogressonreadystatechange 方法进行了劫持覆写, 使用 try ... catch 语句块包裹传入的 callback

重写逻辑如下

fill(XMLHttpRequest.prototype, 'send', _wrapXHR);

function _wrapXHR(originalSend) {
return function () {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
var xhr = this;
var xmlHttpRequestProps = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];
// 劫持覆写
xmlHttpRequestProps.forEach(function (prop) {
if (prop in xhr && typeof xhr[prop] === 'function') {
// 覆写
fill(xhr, prop, function (original) {
var wrapOptions = {
mechanism: {
data: {
// 回调触发的阶段
function: prop,
handler: getFunctionName(original),
},
handled: true,
type: 'instrument',
},
};
var originalFunction = getOriginalFunction(original);
if (originalFunction) {
wrapOptions.mechanism.data.handler = getFunctionName(originalFunction);
}
return wrap$1(original, wrapOptions);
});
}
});
return originalSend.apply(this, args);
};

callback 内部发生异常时,会被 catch 捕获,捕获的异常会被标记对应的请求阶段。