本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前语
自从知道了 Sentry、Fundbug 可用于反常监控之后,小编就一直对它们能主动捕获前端反常的机制十分感兴趣。最近为了处理 Qiankun 下 Sentry 反常上报不匹配的问题,小编特意去翻阅了一下 Sentry 的源代码,在处理了问题的一同,也对 Sentry 反常上报的机制有了一个明晰的认识,收成满满。
在这儿,小编将自己学习所得总结出来,查漏补缺的一同,也希望能给到同样对 Sentry 作业机制感兴趣的同学一些协助。
本文的目录结构如下:
-
常见的前端反常及其捕获办法
-
js 代码履行时反常
-
promise 类反常
-
静态资源加载类型反常
-
接口恳求类型反常
-
跨域脚本履行反常
-
-
Sentry 反常监控原理
-
有效的反常监控需求哪些必备要素
-
反常概况获取
-
用户行为获取
-
-
完毕语
常见的前端反常及其捕获办法
在了解 Sentry 主动捕获反常的机制之前,小编先带咱们了解一下常见的前端反常类型以及各自能够被捕获的办法。
前端反常一般能够分为以下几种类型:
-
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 反常监控原理
了解了常见的前端反常类型以及各自能够被捕获的办法之后,咱们接下来就一同看看 Sentry 是如何做反常监控。
这时分,应该现已有不少小伙伴能够猜到 Sentry 进行反常监控的作业原理了吧,是不是便是咱们在 Sentry 反常监控原理 章节中说到的各类型反常大局捕获办法的汇总呢?
是的,咱们猜的没错,根本上便是这样的,。
不过尽管原理咱们现已知道了,但是 Sentry 内部依旧有不少奇妙的完成能够拿来讲一下的。在这一章节,小编就跟咱们一同聊聊 Sentry 反常监控的原理。
有效的反常监控需求哪些必备要素
反常监控的核心作用便是经过上报的反常,帮开发人员及时发现线上问题并快速修正。
要达到这个意图,反常监控需求做到以下 3 点:
-
线上运用呈现反常时,能够及时推送给开发人员,安排相关人员去处理。
-
上报的反常,含有反常类型、产生反常的源文件及行列信息、反常的追寻栈信息等详细信息,能够协助开发人员快速定位问题。
-
能够获取产生反常的用户行为,协助开发人员、测验人员重现问题和测验回归。
这三点,别离对应反常主动推送、反常概况获取、用户行为获取。
关于反常推送,小编在 凭借飞书捷径,我快速完成了 Sentry 上报反常的主动推送,点赞! 一文中现已做了详细阐明,感兴趣的小伙伴能够去看看,在这儿咱们就不再做过多的阐明。
接下来,咱们就要点聊一聊反常概况获取和用户行为获取。
反常概况获取
为了能主动捕获运用反常,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捕获,捕获的反常会被符号对应的恳求阶段。
有了这些回调上下文信息的协助,定位反常就愈加方便快捷了。
用户行为获取
常见的用户行为,能够归纳为页面跳转、鼠标 click 行为、键盘 keypress 行为、 fetch / xhr 接口恳求、console 打印信息。
Sentry 接入运用今后,会在用户运用运用的进程中,将上述行为逐个搜集起来。等到捕获到反常时,会将搜集到的用户行为和反常信息一同上报。
那 Sentry 是怎么完成搜集用户行为的呢?答案: 绑架覆写上述操作触及的 api。
详细完成进程如下:
-
搜集页面跳转行为
为了能够搜集用户页面跳转行为,
Sentry绑架并覆写了原生history的pushState、replaceState办法和window的onpopstate。绑架覆写
onpopstate:// 运用 oldPopState 变量保存原生的 onpopstate var oldPopState = window.onpopstate; var lastHref; // 覆写 onpopstate window.onpopstate = function() { ... var to = window.location.href; var from = lastHref; lastHref = to; // 将页面跳转行为搜集起来 triggerHandlers('history', { from: from, to: to, }); if (oldOnPopState) { try { // 运用原生的 popstate return oldOnPopState.apply(this, args); } catch (e) { ... } } ... }绑架覆写
pushState、replaceState:// 保存原生的 pushState 办法 var originPushState = window.history.pushState; // 保存原生的 replaceState 办法 var originReplaceState = window.history.replaceState; // 绑架覆写 pushState window.history.pushState = function() { var args = []; for (var i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } var url = args.length > 2 ? args[2] : undefined; if (url) { var from = lastHref; var to = String(url); lastHref = to; // 将页面跳转行为搜集起来 triggerHandlers('history', { from: from, to: to, }); } // 运用原生的 pushState 做页面跳转 return originPushState.apply(this, args); } // 绑架覆写 replaceState window.history.replaceState = function() { var args = []; for (var i = 0; i < arguments.length; i++) { args[i] = arguments[i]; } var url = args.length > 2 ? args[2] : undefined; if (url) { var from = lastHref; var to = String(url); lastHref = to; // 将页面跳转行为搜集起来 triggerHandlers('history', { from: from, to: to, }); } // 运用原生的 replaceState 做页面跳转 return originReplaceState.apply(this, args); } -
搜集鼠标
click/ 键盘keypress行为为了搜集用户鼠标
click和键盘keypress行为,Sentry做了双保险操作:-
经过
document署理click、keypress事情来搜集click、keypress行为; -
经过绑架
addEventListener办法来搜集click、keypress行为;
相关代码完成如下:
function instrumentDOM() { ... // triggerDOMHandler 用来搜集用户 click / keypress 行为 var triggerDOMHandler = triggerHandlers.bind(null, 'dom'); var globalDOMEventHandler = makeDOMEventHandler(triggerDOMHandler, true); // 经过 document 署理 click、keypress 事情的办法搜集 click、keypress 行为 document.addEventListener('click', globalDOMEventHandler, false); document.addEventListener('keypress', globalDOMEventHandler, false); ['EventTarget', 'Node'].forEach(function (target) { var proto = window[target] && window[target].prototype; if (!proto || !proto.hasOwnProperty || !proto.hasOwnProperty('addEventListener')) { return; } // 绑架覆写 Node.prototype.addEventListener 和 EventTarget.prototype.addEventListener fill(proto, 'addEventListener', function (originalAddEventListener) { // 回来新的 addEventListener 覆写原生的 addEventListener return function (type, listener, options) { // click、keypress 事情,要做特殊处理, if (type === 'click' || type == 'keypress') { try { var el = this; var handlers_1 = (el.__sentry_instrumentation_handlers__ = el.__sentry_instrumentation_handlers__ || {}); var handlerForType = (handlers_1[type] = handlers_1[type] || { refCount: 0 }); // 假如没有搜集过 click、keypress 行为 if (!handlerForType.handler) { var handler = makeDOMEventHandler(triggerDOMHandler); handlerForType.handler = handler; originalAddEventListener.call(this, type, handler, options); } handlerForType.refCount += 1; } catch (e) { // Accessing dom properties is always fragile. // Also allows us to skip `addEventListenrs` calls with no proper `this` context. } } // 运用原生的 addEventListener 办法注册事情 return originalAddEventListener.call(this, type, listener, options); }; }); ... }); }整个完成进程仍是十分奇妙的,很值得拿来细细阐明。
首要,
Sentry运用document署理了click、keypress事情。经过这种办法,用户的click、keypress行为能够被感知,然后被Sentry搜集。但这种办法有一个问题,假如运用的
dom节点是经过addEventListener注册了click、keypress事情,而且在事情回调中做了阻止事情冒泡的操作,那么就无法经过署理的办法监控到click、keypress事情了。针对这一种状况,
Sentry采用了覆写Node.prototype.addEventListener的办法来监控用户的click、keypress行为。因为一切的
dom节点都继承自Node目标,Sentry绑架覆写了Node.prototype.addEventListener。当运用代码经过addEventListener订阅事情时,会运用覆写今后的addEventListener办法。新的
addEventListener办法,内部里边也有很奇妙的完成。假如不是click、keypress事情,会直接运用原生的addEventListener办法注册运用提供的listener。但假如是click、keypress事情,除了运用原生的addEventListener办法注册运用提供的listener外,还运用原生addEventListener注册了一个handler,这个handler履行的时分会将用户click、keypress行为搜集起来。也便是说,假如是
click、keypress事情,运用程序在调用addEventListener的时分,实践上是调用了两次原生的addEventListener。真心为这个完成方案点赞!
另外,在搜集
click、keypress行为时,Sentry还会把target节点的的父节点信息搜集起来,协助咱们快速定位节点方位。 -
-
搜集
fetch/xhr接口恳求行为同理,为了搜集运用的接口恳求行为,
Sentry对原生的fetch和xhr做了绑架覆写。绑架覆写
fetch:var originFetch = window.fetch; window.fetch = function() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } // 获取接口 url、method 类型、参数、接口调用时刻信息 var handlerData = { args: args, fetchData: { method: getFetchMethod(args), url: getFetchUrl(args), }, startTimestamp: Date.now(), }; // 搜集接口调用信息 triggerHandlers('fetch', __assign({}, handlerData)); return originalFetch.apply(window, args).then(function (response) { // 接口恳求成功,搜集回来数据 triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response })); return response; }, function (error) { // 接口恳求失利,搜集接口反常数据 triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: error })); throw error; }); }运用中运用
fetch建议恳求时,实践运用的是新的fetch办法。新的fetch内部,会运用原生的fetch建议恳求,并搜集接口恳求数据和回来成果。绑架覆写
xhr:function instrumentXHR() { ... var xhrproto = XMLHttpRequest.prototype; // 覆写 XMLHttpRequest.prototype.open fill(xhrproto, 'open', function (originalOpen) { return function () { ... var onreadystatechangeHandler = function () { if (xhr.readyState === 4) { ... // 搜集接口调用成果 triggerHandlers('xhr', { args: args, endTimestamp: Date.now(), startTimestamp: Date.now(), xhr: xhr, }); } }; // 覆写 onreadystatechange if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') { fill(xhr, 'onreadystatechange', function (original) { return function () { var readyStateArgs = []; for (var _i = 0; _i < arguments.length; _i++) { readyStateArgs[_i] = arguments[_i]; } onreadystatechangeHandler(); return original.apply(xhr, readyStateArgs); }; }); } else { xhr.addEventListener('readystatechange', onreadystatechangeHandler); } return originalOpen.apply(xhr, args); }; }); // 覆写 XMLHttpRequest.prototype.send fill(xhrproto, 'send', function (originalSend) { return function () { ... // 搜集接口调用行为 triggerHandlers('xhr', { args: args, startTimestamp: Date.now(), xhr: this, }); return originalSend.apply(this, args); }; }); }Sentry是经过绑架覆写XMLHttpRequest原型上的open、send办法的办法来完成搜集接口恳求行为的。当运用代码中调用
open办法时,实践运用的是覆写今后的open办法。在新的open办法内部,又覆写了onreadystatechange,这样就能够搜集到接口恳求回来的成果。新的open办法内部会运用调用原生的open办法。同样的,当运用代码中调用
send办法时,实践运用的是覆写今后的send办法。新的send办法内部先搜集接口调用信息,然后调用原生的send办法。 -
搜集
console打印行为有了前面的衬托,
console行为的搜集机制了解起来就十分简略了,实践便是对console的debug、info、warn、error、log、assert这借个api进行绑架覆写。代码如下:
var originConsoleLog = console.log; console.log = function() { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } // 搜集 console.log 行为 triggerHandlers('console', { args: args, level: 'log' }); if (originConsoleLog) { originConsoleLog.apply(console, args); } }
有了这些用户行为信息,咱们就能够依葫芦画瓢,在测验环境复现同类问题了。
完毕语
到这儿,关于 Sentry 完成前端反常监控的介绍就完毕了。
比照第一章节和第二章节,咱们能够发现 Sentry 内部并没有捕获静态资源加载反常的完成。不过没有关系,咱们能够在运用程序中,经过 Sentry 提供的 captureException 这个 api,手动上报反常,十分方便。
最后来一句,假如觉得本文还不错,要及得给小编点个赞哈,。
