本文为稀土技能社区首发签约文章,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
,手动上报反常,十分方便。
最后来一句,假如觉得本文还不错,要及得给小编点个赞哈,。