认识
一、认识
异常监控的核心作用就是通过上报的异常,帮开发人员及时发现线上问题并快速修复。要达到这个目的,异常监控需要做到以下 3
点:
-
线上应用出现异常时,可以及时推送给开发人员,安排相关人员去处理。
-
上报的异常,含有异常类型、发生异常的源文件及行列信息、异常的追踪栈信息等详细信息,可以帮助开发人员快速定位问题。
-
可以获取发生异常的用户行为,帮助开发人员、测试人员重现问题和测试回归。
这三点,分别对应异常自动推送、异常详情获取、用户行为获取。
Sentry
异常监控原理是如何做的呢?
-
为了能自动捕获应用异常,
Sentry
劫持覆写了window.onerror
和window.unhandledrejection
这两个api
-
Sentry
内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括:dom
节点事件回调、setTimeout
/setInterval
回调、xhr
接口调用、requestAnimationFrame
回调等
二、劫持、重写异常捕获API
为了能自动捕获应用异常,Sentry
劫持覆写了 window.onerror
和 window.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
方法来自 Node
的 prototype
对象。为了标记 dom
事件 handler
,Sentry
对 Node.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 name
、event target
等信息。
3.5 xhr 接口回调
标记 xhr
接口回调, 为了标记 xhr
接口回调,Sentry
先对 XMLHttpRequest.prototype.send
方法劫持覆写, 等 xhr
实例使用覆写以后的 send
方法时,再对 xhr
对象的 onload
、onerror
、onprogress
、onreadystatechange
方法进行了劫持覆写, 使用 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
捕获,捕获的异常会被标记对应的请求阶段。