sentry-javascript解析(三)js异常错误如何捕获

779 阅读4分钟

前言

之前讨论了fetchXHR是如何捕获的,本次我们来看看js异常捕获是如何做到的。

关于addInstrumentationHandlerfill方法可以在第一篇文章中了解。

之前的文章可以看这里:

前置准备

常见的代码异常

我们列举几个常见的代码异常

js代码异常

function demoA(params) {
	params.func();
}
demoA();

js代码异常

dom操作异常

var node = document.body.children[0];
var nextNode = node.nextSibling;
var div = document.createTextNode('div');
node.insertBefore(div, nextNode);

dom操作异常

Promise异常

new Promise((resolve, reject) => {
	reject();
});

Promise异常

资源加载异常

const img = document.createElement('img');
img.src = 'abc.png';
document.body.appendChild(img);

资源加载异常

异常错误事件

通过上面的代码我们可以发现不同情况,出现的错误不同,接下来我们看一下这些异常都会触发哪些异常事件。

onerror 和 error 事件

我们最常见的就是window.onerroraddEventListener('error', callback),那么它们两个能捕获哪些异常呢?它们两个又有什么区别呢?mdn - GlobalEventHandlers.onerror

  • window.onerror是一个全局变量,当有js运行时触发错误,window会触发error事件,并执行window.onerror()
  • 监听js运行时错误事件,会比window.onerror先触发,可以全局捕获资源加载异常的错误。

unhandledrejection 事件

Promisereject且没有reject处理器的时候,会触发unhandledrejection事件。mdn - unhandledrejection

js异常错误捕获

接下来我们看一下sentry是如何做异常捕获的。

onerror捕获

高阶函数封装onerror

按照addInstrumentationHandler的代码我们可以准确看出通过type: 'error'接下来应该执行instrumentError方法,我们来看一下这个方法的代码:

// 全局闭包缓存当前window.onerror
let _oldOnErrorHandler: OnErrorEventHandler = null;

function instrumentError(): void {
  // 全局闭包缓存当前window.onerror
  _oldOnErrorHandler = global.onerror;
  // 重置window.onerror
  global.onerror = function(msg, url, line, column, error): boolean {
    // 遍历onerror对应的回调
    triggerHandlers('error', {
      column,
      error,
      line,
      msg,
      url,
    });
    // 如果当前已经有设置onerror则继续调用执行
    if (_oldOnErrorHandler) {
      return _oldOnErrorHandler.apply(this, arguments);
    }

    return false;
  };
}

onerror对应回调

我们了解完onerror是如何封装之后,再来看看对应的回调里都做了什么。我们可以在@sentry/browsersrc/integrations/globalhandlers.ts中找到对应的代码。

 addInstrumentationHandler({
      callback: (data: { msg; url; line; column; error }) => {
        const error = data.error;
        const currentHub = getCurrentHub();
        const hasIntegration = currentHub.getIntegration(GlobalHandlers);
        const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;

        if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {
          return;
        }

        const client = currentHub.getClient();
        // isPrimitive用于判断error的数据类型是否为原始数据类型
        // 这里根据error数据类型不同,拼接成统一的数据结构
        const event = isPrimitive(error)
          ? this._eventFromIncompleteOnError(data.msg, data.url, data.line, data.column)
          : this._enhanceEventWithInitialFrame(
              eventFromUnknownInput(error, undefined, {
                attachStacktrace: client && client.getOptions().attachStacktrace,
                rejection: false,
              }),
              data.url,
              data.line,
              data.column,
            );

        addExceptionMechanism(event, {
          handled: false,
          type: 'onerror',
        });
        // 记录上报本次事件
        currentHub.captureEvent(event, {
          originalException: error,
        });
      },
      type: 'error',
});

onerror的回调比较简单,简单来说就是根据捕获到的error对象数据类型不同,经过特定的整合最终输出统一的数据结构上报就结束了。

unhandledrejection捕获

高阶函数封装unhandledrejection

按照addInstrumentationHandler的代码我们可以准确看出通过type: 'unhandledrejection'接下来应该执行instrumentUnhandledRejection方法,我们来看一下这个方法的代码:

// 全局闭包缓存当前window.onunhandledrejection
let _oldOnUnhandledRejectionHandler: ((e: any) => void) | null = null;

function instrumentUnhandledRejection(): void {
  // 全局闭包缓存当前window.onunhandledrejection
  _oldOnUnhandledRejectionHandler = global.onunhandledrejection;
  // 重置window.onunhandledrejection
  global.onunhandledrejection = function(e: any): boolean {
    // 遍历unhandledrejection对应回调
    triggerHandlers('unhandledrejection', e);
    // 如果当前已经有设置unhandledrejection则继续调用执行
    if (_oldOnUnhandledRejectionHandler) {
      return _oldOnUnhandledRejectionHandler.apply(this, arguments);
    }

    return true;
  };
}

unhandledrejection对应回调

我们了解完unhandledrejection是如何封装之后,再来看看对应的回调里都做了什么。我们可以在@sentry/browser的src/integrations/globalhandlers.ts中找到对应的代码。

addInstrumentationHandler({
      callback: (e: any) => {
        let error = e;

        try {
          // 判断error是否包含reason字段
          // 详情参考https://developer.mozilla.org/zh-CN/docs/Web/API/PromiseRejectionEvent
          if ('reason' in e) {
            error = e.reason;
          } else if ('detail' in e && 'reason' in e.detail) {
            // 这里兼容自定义事件获取对应的reason
            // 详情参考https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent
            error = e.detail.reason;
          }
        } catch (_oO) {
        }

        const currentHub = getCurrentHub();
        const hasIntegration = currentHub.getIntegration(GlobalHandlers);
        const isFailedOwnDelivery = error && error.__sentry_own_request__ === true;

        if (!hasIntegration || shouldIgnoreOnError() || isFailedOwnDelivery) {
          return true;
        }

        const client = currentHub.getClient();
        // isPrimitive用于判断error的数据类型是否为原始数据类型
        // 这里根据error数据类型不同,拼接成统一的数据结构
        const event = isPrimitive(error)
          ? this._eventFromRejectionWithPrimitive(error)
          : eventFromUnknownInput(error, undefined, {
              attachStacktrace: client && client.getOptions().attachStacktrace,
              rejection: true,
            });

        event.level = Severity.Error;

        addExceptionMechanism(event, {
          handled: false,
          type: 'onunhandledrejection',
        });
        // 记录上报本次事件
        currentHub.captureEvent(event, {
          originalException: error,
        });

        return;
      },
      type: 'unhandledrejection',
    });

我们发现onunhandledrejection也是比较简单,根据捕获到的error数据结构不同,经过特定的整合最终输出统一的数据结构上报就结束了。

onerror的区别是兼容考虑了不同的场景:

  • 捕获到的error包含reason字段,则以reason为主
  • 捕获到的error是继承的CustomEvent自定义事件,则以detail.reason为主

总结

sentry在异常错误捕获会通过onerroronunhandledrejection两个方法进行监听。高阶函数封装也相对比较简单,提前执行sentry对应的回调上报异常事件。