本文为稀土技能社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前语

自从知道了 SentryFundbug 可用于反常监控之后,小编就一直对它们能主动捕获前端反常的机制十分感兴趣。最近为了处理 QiankunSentry 反常上报不匹配的问题,小编特意去翻阅了一下 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 时,假如 promisereject 但没有做 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) 的办法去大局捕获。

静态资源加载类型反常

有的时分,假如咱们页面的imgjscss 等资源链接失效,就会提示资源类型加载如反常。

<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 点:

  1. 线上运用呈现反常时,能够及时推送给开发人员,安排相关人员去处理。

  2. 上报的反常,含有反常类型、产生反常的源文件及行列信息、反常的追寻栈信息等详细信息,能够协助开发人员快速定位问题。

  3. 能够获取产生反常的用户行为,协助开发人员、测验人员重现问题和测验回归。

这三点,别离对应反常主动推送反常概况获取用户行为获取

关于反常推送,小编在 凭借飞书捷径,我快速完成了 Sentry 上报反常的主动推送,点赞! 一文中现已做了详细阐明,感兴趣的小伙伴能够去看看,在这儿咱们就不再做过多的阐明。

接下来,咱们就要点聊一聊反常概况获取和用户行为获取。

反常概况获取

为了能主动捕获运用反常,Sentry 绑架覆写了 window.onerrorwindow.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.onerrorwindow.unhandledrejection 已足以完成反常主动捕获,但为了能获取更详尽的反常信息, Sentry 在内部做了一些更纤细的反常捕获。

详细来说,便是 Sentry 内部对反常产生的特殊上下文,做了符号。这些特殊上下文包含: dom 节点事情回调、setTimeout / setInterval 回调、xhr 接口调用、requestAnimationFrame 回调等。

举个 ,假如是 click 事情的 handler 中产生了反常, Sentry 会捕获这个反常,并将反常产生时的事情 namedom 节点描述、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

    因为 setIntervalrequestAnimationFrame 的绑架覆写逻辑和 setTimeout 根本一样,这儿就不再重复阐明晰,感兴趣的小伙伴们可自行完成。

  • 符号 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 等信息。

    其实,除了符号 dom 事情回调上下文,Sentry 还能够符号 NotificationWebSocketXMLHttpRequest 等目标的事情回调上下文。能够这么说,只要一个目标有 addEventListener 办法而且能够被绑架覆写,那么对应的回调上下文会能够被符号。

  • 符号 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 捕获,捕获的反常会被符号对应的恳求阶段。

有了这些回调上下文信息的协助,定位反常就愈加方便快捷了。

用户行为获取

常见的用户行为,能够归纳为页面跳转鼠标 click 行为键盘 keypress 行为fetch / xhr 接口恳求console 打印信息

Sentry 接入运用今后,会在用户运用运用的进程中,将上述行为逐个搜集起来。等到捕获到反常时,会将搜集到的用户行为和反常信息一同上报。

Sentry 是怎么完成搜集用户行为的呢?答案: 绑架覆写上述操作触及的 api

详细完成进程如下:

  • 搜集页面跳转行为

    为了能够搜集用户页面跳转行为,Sentry 绑架并覆写了原生 historypushStatereplaceState 办法和 windowonpopstate

    绑架覆写 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) {
                ...
            }
        }
        ...
    }
    

    绑架覆写 pushStatereplaceState

    // 保存原生的 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 署理 clickkeypress 事情来搜集 clickkeypress 行为;

    • 经过绑架 addEventListener 办法来搜集 clickkeypress 行为;

    相关代码完成如下:

    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 署理了 clickkeypress 事情。经过这种办法,用户的 clickkeypress 行为能够被感知,然后被 Sentry 搜集。

    但这种办法有一个问题,假如运用的 dom 节点是经过 addEventListener 注册了 clickkeypress 事情,而且在事情回调中做了阻止事情冒泡的操作,那么就无法经过署理的办法监控到 clickkeypress 事情了。

    针对这一种状况, Sentry 采用了覆写 Node.prototype.addEventListener 的办法来监控用户的 clickkeypress 行为。

    因为一切的 dom 节点都继承自 Node 目标,Sentry 绑架覆写了 Node.prototype.addEventListener。当运用代码经过 addEventListener 订阅事情时,会运用覆写今后的 addEventListener 办法。

    新的 addEventListener 办法,内部里边也有很奇妙的完成。假如不是 clickkeypress 事情,会直接运用原生的 addEventListener 办法注册运用提供的 listener。但假如是 clickkeypress 事情,除了运用原生的 addEventListener 办法注册运用提供的 listener 外,还运用原生 addEventListener 注册了一个 handler,这个 handler 履行的时分会将用户 clickkeypress 行为搜集起来。

    也便是说,假如是 clickkeypress 事情,运用程序在调用 addEventListener 的时分,实践上是调用了两次原生的 addEventListener

    真心为这个完成方案点赞!

    另外,在搜集 clickkeypress 行为时,Sentry 还会把 target 节点的的父节点信息搜集起来,协助咱们快速定位节点方位。

  • 搜集 fetch / xhr 接口恳求行为

    同理,为了搜集运用的接口恳求行为,Sentry 对原生的 fetchxhr 做了绑架覆写。

    绑架覆写 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 原型上的 opensend 办法的办法来完成搜集接口恳求行为的。

    当运用代码中调用 open 办法时,实践运用的是覆写今后的 open 办法。在新的 open 办法内部,又覆写了 onreadystatechange,这样就能够搜集到接口恳求回来的成果。新的 open 办法内部会运用调用原生的 open 办法。

    同样的,当运用代码中调用 send 办法时,实践运用的是覆写今后的 send 办法。新的 send 办法内部先搜集接口调用信息,然后调用原生的 send 办法。

  • 搜集 console 打印行为

    有了前面的衬托,console 行为的搜集机制了解起来就十分简略了,实践便是对 consoledebuginfowarnerrorlogassert 这借个 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,手动上报反常,十分方便。

最后来一句,假如觉得本文还不错,要及得给小编点个赞哈,。