51Testing软件测试论坛
标题:
Sentry如何自动捕获前端应用异常原理?
[打印本页]
作者:
草帽路飞UU
时间:
2022-12-13 16:05
标题:
Sentry如何自动捕获前端应用异常原理?
本帖最后由 草帽路飞UU 于 2022-12-13 16:06 编辑
常见的
前端
异常及其捕获方式
前端异常通常可以分为以下几种类型:
·
js 代码执行时异常;
·
promise 类型异常;
·
资源加载类型异常;
·
网络请求类型异常;
·
跨域脚本执行异常;
·
不同类型的异常,捕获方式不同。
js 代码执行时异常
js 代码执行异常,是我们经常遇到异常。这一类型的异常,又可以具体细分为:
·
Error,最基本的错误类型,其他的错误类型都继承自该类型。通过 Error,我们可以自定义 Error 类型。
·
RangeError: 范围错误。当出现堆栈溢出(递归没有终止条件)、数值超出范围(new Array 传入负数或者一个特别大的整数)情况时会抛出这个异常。
·
ReferenceError,引用错误。当一个不存在的对象被引用时发生的异常。
·
SyntaxError,语法错误。如变量以数字开头;花括号没有闭合等。
·
TypeError,类型错误。如把 number 当 str 使用。
·
URIError,向全局 URI 处理函数传递一个不合法的 URI 时,就会抛出这个异常。如使用 decodeURI('%')、decodeURIComponent('%')。
·
EvalError, 一个关于 eval 的异常,不会被 javascript 抛出。
具体详见: Error - JavaScript - MDN Web Docs - Mozilla
通常,我们会通过 try...catch 语句块来捕获这一类型异常。如果不使用 try...catch,我们也可以通过 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式进行全局捕
获。
promise 类异常
在使用 promise 时,如果 promise 被 reject 但没有做 catch 处理时,就会抛出 promise 类异常。
Promise.reject(); // Uncaught (in promise) undefined
promise 类型的异常无法被 try...catch 捕获,也无法被 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式全局捕获。针对这一类型的异常, 我们需要通过
window.onrejectionhandled = callback 或者 window.addListener('rejectionhandled', callback) 的方式去全局捕获。
静态资源加载类型异常
如果我们页面的img、js、css 等资源链接失效,就会提示资源类型加载如异常。
<img src="localhost:3000/data.png" /> // Get localhost:3000/data.png net::ERR_FILE_NOT_FOUND
针对这一类的异常,我们可以通过 window.addEventListener('error', callback, true) 的方式进行全局捕获。
这里要注意一点,使用 window.onerror = callback 的方式是无法捕获静态资源类异常的。
原因是资源类型错误没有冒泡,只能在捕获阶段捕获,而 window.onerror 是通过在冒泡阶段捕获错误,对静态资源加载类型异常无效,所以只能借助 window.addEventListener('error', callback, true)
的方式捕获。
接口请求类型异常
在浏览器端发起一
个接口请求时,如果请求的 url 的有问题,也会抛出异常。
不同的请求方式,异常捕获方式也不相同:
·
接口调用是通过 fetch 发起的
我们可以通过 fetch(url).then(callback).catch(callback) 的方式去捕获异常。
·
接口调用通过 xhr 实例发起
如果是 xhr.open 方法执行时出现异常,可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。
xhr.open('GET', "https://") // Uncaught DOMException: Failed to execute 'open' on 'XMLHttpRequest': Invalid URL
at ....
如果是 xhr.send 方法执行时出现异常,可以通过 xhr.onerror 或者 xhr.addEventListener('error', callback) 的方式捕获异常。
xhr.open('get', '/user/userInfo');
xhr.send(); // send localhost:3000/user/userinfo net::ERR_FAILED
跨域脚本执行异常
当项目中引用的第三方脚本执行发生错误时,会抛出一类特殊的异常。这类型异常和我们刚才讲过的异常都不同,它的 msg 只有 'Script error' 信息,没有具体的行、列、类型信息。
之以会这样,是因为浏览器的安全机制: 浏览器只允许同域下的脚本捕获具体异常信息,跨域脚本中的异常,不会报告错误的细节。
针对这类型的异常,我们可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。
如果我们想获取这类异常的详情,需要做以下两个操作:
·
在发起请求的 script 标签上添加 crossorigin="anonymous";
·
请求响应头中添加 Access-Control-Allow-Origin: *;
这样就可以获取到跨域异常的细节信息了。
Sentry 异常监控原理
有效的异常监控需要哪些必备要素
异常监控的核心作用就是通过上报的异常,帮开发人员及时发现线上问题并快速修复。
要达到这个目的,异常监控需要做到以下 3 点:
线上应用出现异常时,可以及时推送给开发人员,安排相关人员去处理。
上报的异常,含有异常类型、发生异常的源文件及行列信息、异常的追踪栈信息等详细信息,可以帮助开发人员快速定位问题。
可以获取发生异常的用户行为,帮助开发人员、测试人员重现问题和测试回归。
这三点,分别对应异常自动推送、异常详情获取、用户行为获取。
异常详情获取
为了能自动捕获应用异常,Sentry 劫持覆写了 window.onerror 和 window.unhandledrejection 这两个 api。
劫持覆写 window.onerror 的代码如下:
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;
};
劫持覆写 window.unhandledrejection 的代码如下:
oldOnUnhandledRejectionHandler = window.onunhandledrejection;
window.onunhandledrejection = function (e) {
// 收集异常信息并上报
triggerHandlers('unhandledrejection', e);
if (oldOnUnhandledRejectionHandler) {
return oldOnUnhandledRejectionHandler.apply(this, arguments);
}
return true;
};
虽然通过劫持覆写 window.onerror 和 window.unhandledrejection 已足以完成异常自动捕获,但为了能获取更详尽的异常信息, Sentry 在内部做了一些更细微的异常捕获。
具体来说,就是 Sentry 内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括: dom 节点事件回调、setTimeout / setInterval 回调、xhr 接口调用、requestAnimationFrame 回调等。
举个 ,如果是 click 事件的 handler 中发生了异常, Sentry 会捕获这个异常,并将异常发生时的事件 name、dom 节点描述、handler 函数名等信息上报。
具体处理逻辑如下:
·
标记 setTimeout / setInterval / requestAnimationFrame
·
为了标记 setTimeout / setInterval / requestAnimationFrame 类型的异常,Sentry 劫持覆写了原生的 setTimout / setInterval / requestAnimationFrame 方法。新的 setTimeout / setInterval /
requestAnimationFrame 方法调用时,会使用 try ... catch 语句块包裹 callback。
具体实现如下:
var 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。
·
由于 setInterval、requestAnimationFrame 的劫持覆写逻辑和 setTimeout 基本一样,这里就不再重复说明了,感兴趣的小伙伴们可自行实现。
·
标记 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 等信息。
其实,除了标记 dom 事件回调上下文,Sentry 还可以标记 Notification、WebSocket、XMLHttpRequest 等对象的事件回调上下文。可以这么说,只要一个对象有 addEventListener 方法并且可以被
劫持覆写,那么对应的回调上下文会可以被标记。
标记 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 捕获,捕获的异常会被标记对应的请求阶段。
有了这些回调上下文信息的帮助,定位异常就更加方便快捷了。
欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/)
Powered by Discuz! X3.2