阅读 858

解析Sentry源码(二)| Sentry如何处理错误数据

一、前言

最近对前端监控很有兴趣,所以去使用了前端监控里优秀的开源库Sentry,并且研究了下Sentry源码,整理出这一系列的文章,希望能帮助大家更好地了解前端监控原理。

这一系列的文章我将结合官网api与流程图的方式,通俗易懂地讲解整个流程。

以下是我已完成和计划下一篇文章的主题:


关于Sentry源码如何调试大家可以按照如下(说明我的版本为:"5.29.2"):

  • git clone git@github.com:getsentry/sentry-javascript.git
  • 进入到 packages/browser 进行npm i 下载依赖
  • 然后在packages/browser 进行npm run build 去打包,生成build文件夹
  • 进入到 packages/browser/examples 打开index.html就可以开心调试了(这里建议用Live Server打开)
  • 说明:packages/browser/examples 下的bundle.js就是打包后的源码,他指向了packages/browser/build/bundle.js。这时候你会发现build目录下还有bundle.es6.js,如果你想使用es6的方式去阅读可以将文件替换成bundle.es6.js

二、导读

我们先来了解一下前端错误的基本内容:

2.1 解前端常见的错误类型有几种:

(1) ECMAScript Execeptions

当脚本代码运行时发生的错误,Error的实例对象会被抛出,除了通用的Error构造函数外,JavaScript还有7个其他类型的错误构造函数

  • SyntaxError: 语法错误指的使用了未定义或错误的语法而引发的异常。语法错误会在编译或者解析源码的过程中被检测出来。
  • ReferenceError: 引用错误代表当一个不存在的变量被引用时发生的错误。
  • RangeError:范围错误代表当一个值不在其所允许的范围或者集合中。
  • TypeError:类型错误用来表示值的类型非预期类型时发生的错误。
  • URIError: URL错误用来表示以一种错误的方式使用全局URI处理函数而产生的错误。
  • EvalError:eval函数的错误代表了一个关于eval函数的错误.此异常不再会被JavaScript抛出,但是EvalError对象仍然保持兼容性。
  • InternalError:引擎错误表示出现在JavaScript引擎内部的错误(例如:"InternalError: too much recursion"内部错误:递归过深)。

(2)DOMException

最新的DOM规范定义的错误类型集,兼容旧浏览的DOMError接口, 完善和规范化DOM错误类型。

  • IndexSizeError: 索引不在允许的范围内
  • HierarchyRequestError: 节点树层次结构有误。
  • WrongDocumentError: 对象在错误的 Document中。
  • InvalidCharacterError*:字符串包含无效字符。
  • NoModificationAllowedError: 对象不能被修改。
  • NotFoundError: 找不到对象。
  • NotSupportedError: 不支持的操作
  • InvalidStateError: 对象是一个无效的状态。
  • SyntaxError: 字符串不匹配预期的模式。
  • InvalidModificationError: 对象不能被这种方式修改。
  • NamespaceError: 操作在XML命名空间内是不被允许的。
  • InvalidAccessError: 对象不支持这种操作或参数。
  • TypeMismatchError:对象的类型不匹配预期的类型。
  • SecurityError: 此操作不安全的。
  • NetworkError: 发生网络错误。
  • AbortError: 操作被中止。
  • URLMismatchError: 给定的URL不匹配另一个URL。
  • QuotaExceededError:给定配额已经超过了。
  • TimeoutError: 操作超时。
  • InvalidNodeTypeError: 这个操作的节点是不正确的或祖先是不正确的。
  • DataCloneError: 对象不能克隆。

2.2 前端错误异常按照捕获方式分类

(1)脚本错误

脚本错误可以分为编译时错误以及运行时错误

脚本错误的捕获方式有:

  • try catch

    优点:

    • 通过try...catch我们能够知道出错的信息,并且也有堆栈信息可以知道在哪个文件第几行第几列发生错误。

    缺点:

    • 只能捕获同步代码的异常
    • 捕获错误是侵入式的,作为一个监控系统是不合适的
  • window.onerror

    优点:

    • 能全局捕获错误

    • 一样可以拿到出错的信息以及文件名、行号、列号等信息,还可以在window.onerror最后return true让浏览器不输出错误信息到控制台

      /*
      * @param msg{String}:错误消息
      * @param url{String}:引发错误的脚本的URL
      * @param line{Number}:发生错误的代码行
      * @param colunm{Number}:发生错误的代码列
      * @param error{object}:错误对象
      */
      
      window.onerror = function (msg, url, line, colunm, error) {
      	return true;
      }
      复制代码

    缺点:

    • 只能捕获即时运行错误,不能捕获资源加载错误(原理:资源加载错误,没有冒泡所以并不会向上冒泡,object.onerror捕获后就会终止,所以window.onerror并不能捕获资源加载错误

Script跨域脚本错误捕获:

为了性能方面的考虑,我们一般会将脚本文件放到 CDN ,这种方法会大大加快首屏时间。但是,如果脚本报错,此时,浏览器出于于安全方面的考虑,对于不同源的脚本报错,无法捕获到详细错误信息,只会显示 Script Error

解决方案:

  • 可以在 script 标签中,添加 crossorigin 属性(推荐使用 webpack 插件自动添加)。
  • 服务端设置js资源响应头Access-Control-Origin:*(或者是域名)

(2)资源加载错误

资源加载错误包括:img、script、link、audio、video、iframe ...

资源加载与脚本错误异同点:

相同点:

  • 监控资源错误本质上和监控常规脚本错误一样,都是监控错误事件实现错误捕获。

不同点:

  • 脚本错误参数对象 instanceof ErrorEvent,而资源错误的参数对象 instanceof Event。(由于 ErrorEvent 继承于 Event ,所以不管是脚本错误还是资源错误的参数对象,它们都 instanceof Event,所以,需要先判断脚本错误)。
  • 脚本错误的参数对象中包含 message ,而资源错误没有 。

资源加载捕获方式:

  • object.onerror:如:img标签、script标签都可以添加onerror事件,用来捕获资源加载错误

  • performance.getEntries:通过performance.getEntries可以获取网站成功加载的资源数量信息

    所以通过allImsloadedImgs对比即可找出图片资源未加载项目

    const allImgs = document.getElementsByTagName('image')
    const loadedImgs = performance.getEntries().filter(i => i.initiatorType === 'img')
    复制代码

(3) Promise 错误

上述的try catch和window.onerror是无法捕捉Promise错误的(因为是异步)
而当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件
Promise 被 reject 且有 reject 处理器的时候,会触发 rejectionhandled 事件。
说明:Sentry这边只收集没有被reject的错误即window.unhandledrejection

2.3 上报错误方式

  • 采用Ajax通信的方式上报(Sentry采用的方式)

  • img请求上报, url参数带上错误信息

    比如:(new Image()).src = 'docs.sentry.io/error?error…'

三、如何监听错误

我们可以先看下图,本章节将重点如讲解在Sentry初始化的时候,是如何监听错误的,对window.onerror和window.unhandledrejection都做了哪些处理

关于Sentry初始化集成内容可以参考玩转前端监控,全面解析Sentry源码(一)| 搞懂Sentry初始化

监听错误步骤如下:

3.1 初始化

用户使用Sentry.init初始化,传入参数

3.2 绑定控制中心hub

init方法会调用initAndBind去初始化了 client,并且把 client 绑定到了 hub 上,initAndBind中的bindClient方法就是把 client 绑定到了 hub 控制中心上

3.3 集成install

关注bindClient中的setupIntegrations方法

      bindClient(client) {
          const top = this.getStackTop();
          top.client = client;
          if (client && client.setupIntegrations) {
              client.setupIntegrations();
          }
      }
复制代码

setupIntegrations

     setupIntegrations() {
          if (this._isEnabled()) {
              this._integrations = setupIntegrations(this._options);
          }
      }
      
    function setupIntegrations(options) {
        const integrations = {};
        getIntegrationsToSetup(options).forEach(integration => {
            integrations[integration.name] = integration;
            setupIntegration(integration);
        });
        return integrations;
    }
复制代码

分析:

  • setupIntegrations 是对集成integrations进行遍历,把默认集成或者自定义集成进行install
  • getIntegrationsToSetup就是获取integrations
  • 注意:_isEnabled是用户传入的options不能把enabled为false和dsn为空,否则不会采集发送

3.4 setupOnce

接着来看看setupIntegration

  /** Setup given integration */
  function setupIntegration(integration) {
      if (installedIntegrations.indexOf(integration.name) !== -1) {
          return;
      }
      integration.setupOnce(addGlobalEventProcessor, getCurrentHub);
      installedIntegrations.push(integration.name);
      logger.log(`Integration installed: ${integration.name}`);
  }
复制代码

分析:

  • 这里会对集成进行遍历,执行setupOnce方法
  • 默认集成有7个,我们只要关注其中的new GlobalHandlers
  • 当对GlobalHandlers install的时候,会执行其setupOnce方法

setupOnce

    setupOnce() {
      Error.stackTraceLimit = 50;
      if (this._options.onerror) {
        logger.log('Global Handler attached: onerror');
        this._installGlobalOnErrorHandler();
      }
      if (this._options.onunhandledrejection) {
        logger.log('Global Handler attached: onunhandledrejection');
        this._installGlobalOnUnhandledRejectionHandler();
      }
    }
复制代码

分析:

  • 在new GlobalHandlers()的时候会给_options设置onerror和onunhandledrejection默认值为true

到此就开始就可以看到Sentry对于运行错误和Promise错误是分别做不同的处理

3.5 window.onerror

_installGlobalOnErrorHandler对应window.onerror类型

      _installGlobalOnErrorHandler() {
          if (this._onErrorHandlerInstalled) {
              return;
          }
          addInstrumentationHandler({
              callback: (data) => {
              	// ...
              },
              type: 'error',
          });
          this._onErrorHandlerInstalled = true;
      }
复制代码

分析:

  • _installGlobalOnErrorHandler为addInstrumentationHandler方法传入callbacktype
  • 其中里面的callback方法这里与初始化的内容无关,我们会在下一章重点讲解。
  • type类型是为了区分当前类型,在_installGlobalOnUnhandledRejectionHandler会传入type:'unhandledrejection'

3.6 window.unhandledrejection

_installGlobalOnUnhandledRejectionHandler对应window.unhandledrejection

      _installGlobalOnUnhandledRejectionHandler() {
          if (this._onUnhandledRejectionHandlerInstalled) {
              return;
          }
          addInstrumentationHandler({
              callback: (e) => {
            		// ...
              },
              type: 'unhandledrejection',
          });
          this._onUnhandledRejectionHandlerInstalled = true;
      }
复制代码

与_installGlobalOnErrorHandler同理

3.7 addInstrumentationHandler

来看看通用的addInstrumentationHandler方法

  /**
   * Add handler that will be called when given type of instrumentation triggers.
   * Use at your own risk, this might break without changelog notice, only used internally.
   * @hidden
   */
  const handlers = {};
  function addInstrumentationHandler(handler) {
      if (!handler || typeof handler.type !== 'string' || typeof handler.callback !== 'function') 			{
          return;
      }
      handlers[handler.type] = handlers[handler.type] || [];
      handlers[handler.type].push(handler.callback);
      instrument(handler.type);
  }
复制代码

分析:

  • addInstrumentationHandler 会把type与callback传入到handlers中,当发生错误的时候,就会执行对应callback方法
  • addInstrumentationHandler最后会根据type类型去调用instrument方法,从而执行各种不同的方法。

3.8 instrument

通过instrument可以知道Sentry都处理了哪些情况。如果有兴趣其他方面,可以自己查看对应的源码,里面部分内容我也会之后开一个专题讲解。

  function instrument(type) {
    if (instrumented[type]) {
      return;
    }
    instrumented[type] = true;
    switch (type) {
      case 'console':
        instrumentConsole();
        break;
      case 'dom':
        instrumentDOM();
        break;
      case 'xhr':
        instrumentXHR();
        break;
      case 'fetch':
        instrumentFetch();
        break;
      case 'history':
        instrumentHistory();
        break;
      case 'error':
        instrumentError();
        break;
      case 'unhandledrejection':
        instrumentUnhandledRejection();
        break;
      default:
        logger.warn('unknown instrumentation type:', type);
    }
  }
复制代码

3.9 重点:instrumentError

  let _oldOnErrorHandler = null;
  /** JSDoc */
  function instrumentError() {
      _oldOnErrorHandler = global$2.onerror;
      global$2.onerror = function (msg, url, line, column, error) {
          triggerHandlers('error', {
              column,
              error,
              line,
              msg,
              url,
          });
          if (_oldOnErrorHandler) {
              return _oldOnErrorHandler.apply(this, arguments);
          }
          return false;
      };
  }
复制代码

分析:

  • 如果熟悉aop(面向切面编程)的小伙伴对这段代码不会陌生,这里对winodw.onerror 进行劫持,添加了triggerHandlers方法。
  • 而当监听到onerror的时候,会调用triggerHandlers 方法根据类型’error'会到handlers中找到对应类型的callback方法,也就是_installGlobalOnErrorHandler的callback方法

3.10 重点:instrumentUnhandledRejection。

  function instrumentUnhandledRejection() {
      _oldOnUnhandledRejectionHandler = global$2.onunhandledrejection;
      global$2.onunhandledrejection = function (e) {
          triggerHandlers('unhandledrejection', e);
          if (_oldOnUnhandledRejectionHandler) {
              // eslint-disable-next-line prefer-rest-params
              return _oldOnUnhandledRejectionHandler.apply(this, arguments);
          }
          return true;
      };
  }
复制代码

与instrumentError同理


到此,已经为完成整个初始化的步骤了。接下来就是重点关注发生各种类型错误的不同输出。

四、Sentry都对错误信息做了哪些处理

现在我们来先看看_installGlobalOnErrorHandler和_installGlobalOnUnhandledRejectionHandler中的callback

_installGlobalOnErrorHandler:

 callback: data => {
          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();
          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,
          });
        },
复制代码

_installGlobalOnUnhandledRejectionHandler

        callback: e => {
          let error = e;
      	// ...
          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();
          const event = isPrimitive(error)
            ? this._eventFromRejectionWithPrimitive(error)
            : eventFromUnknownInput(error, undefined, {
                attachStacktrace: client && client.getOptions().attachStacktrace,
                rejection: true,
              });
          event.level = exports.Severity.Error;
          addExceptionMechanism(event, {
            handled: false,
            type: 'onunhandledrejection',
          });
          currentHub.captureEvent(event, {
            originalException: error,
          });
          return;
        },
复制代码

从源码看起来_installGlobalOnErrorHandler和_installGlobalOnUnhandledRejectionHandler是很类似的,我们统一一起来分析。

4.1 shouldIgnoreOnError

注意这一个函数,我们先看看源码:

 let ignoreOnError = 0;
  function shouldIgnoreOnError() {
    return ignoreOnError > 0;
  }
复制代码

分析:

  • 如果你是直接抛出错误throw new Error的时候(实战那边也会有例子),这时候shouldIgnoreOnError会返回true

  • 因为我们回过头看[instrument](#3.8 instrument)方法,如果type=‘dom’,会调用instrumentDOM方法

  • 所以在初始化的时候已经为为函数function包裹一层wrap方法,于是当直接报错的时候ignoreOnError会+1,这也就是导致会直接跳出callback,不上报错误的原因

    try{
    	...
    }catch{
    	ignoreNextOnError()
    }
    
    ------------------------------------------
      function ignoreNextOnError() {
        // onerror should trigger before setTimeout
        ignoreOnError += 1;
        setTimeout(() => {
          ignoreOnError -= 1;
        });
      }
    复制代码

4.2 _installGlobalOnErrorHandler_installGlobalOnUnhandledRejectionHandler的不同点

虽然这两个函数看起来很多,但其实都是差不多的,只有在对错误处理上有些差别:

_installGlobalOnErrorHandler

          const event = isPrimitive(error)
            ? this._eventFromIncompleteOnError(...)
            : this._enhanceEventWithInitialFrame(
                eventFromUnknownInput(...))
复制代码

_installGlobalOnUnhandledRejectionHandler

          const event = isPrimitive(error)
            ? this._eventFromRejectionWithPrimitive(...)
            : eventFromUnknownInput(...);
复制代码

分析:

  • 通过isPrimitive去判断错误类型是基本呢类型还是引用类型而走不同的操作

所以我们就分成4个模块:

  • eventFromUnknownInput
  • _enhanceEventWithInitialFrame
  • _eventFromIncompleteOnError
  • _eventFromRejectionWithPrimitive

分别来研究都对错误做了哪些处理:

4.3 重点:eventFromUnknownInput

eventFromUnknownInput(error, undefined, {
  attachStacktrace: client && client.getOptions().attachStacktrace,
  rejection: false,
}) 
--------------------------------------------------------------------------------------------
function eventFromUnknownInput(exception, syntheticException, options = {}) {
    let event;
    if (isErrorEvent(exception) && exception.error) {
      // If it is an ErrorEvent with `error` property, extract it to get actual Error
      const errorEvent = exception;
      // eslint-disable-next-line no-param-reassign
      exception = errorEvent.error;
      event = eventFromStacktrace(computeStackTrace(exception));
      return event;
    }
    if (isDOMError(exception) || isDOMException(exception)) {
      // If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
      // then we just extract the name, code, and message, as they don't provide anything else
      // https://developer.mozilla.org/en-US/docs/Web/API/DOMError
      // https://developer.mozilla.org/en-US/docs/Web/API/DOMException
      const domException = exception;
      const name = domException.name || (isDOMError(domException) ? 'DOMError' : 'DOMException');
      const message = domException.message ? `${name}: ${domException.message}` : name;
      event = eventFromString(message, syntheticException, options);
      addExceptionTypeValue(event, message);
      if ('code' in domException) {
        event.tags = Object.assign(Object.assign({}, event.tags), { 'DOMException.code': `${domException.code}` });
      }
      return event;
    }
    if (isError(exception)) {
      // we have a real Error object, do nothing
      event = eventFromStacktrace(computeStackTrace(exception));
      return event;
    }
    if (isPlainObject(exception) || isEvent(exception)) {
      // If it is plain Object or Event, serialize it manually and extract options
      // This will allow us to group events based on top-level keys
      // which is much better than creating new group when any key/value change
      const objectException = exception;
      event = eventFromPlainObject(objectException, syntheticException, options.rejection);
      addExceptionMechanism(event, {
        synthetic: true,
      });
      return event;
    }
    // If none of previous checks were valid, then it means that it's not:
    // - an instance of DOMError
    // - an instance of DOMException
    // - an instance of Event
    // - an instance of Error
    // - a valid ErrorEvent (one with an error property)
    // - a plain Object
    //
    // So bail out and capture it as a simple message:
    event = eventFromString(exception, syntheticException, options);
    addExceptionTypeValue(event, `${exception}`, undefined);
    addExceptionMechanism(event, {
      synthetic: true,
    });
    return event;
  }
复制代码

分析:

  • 参数说明:

    • exception就是错误信息
    • 注意第二个参数为undefind(于是在eventFromPlainObject和eventFromString中传入的syntheticException此时就是undefined!!也就不会生成错误堆栈)
    • 第三个参数中的attachStacktrace这边是用户在Sentry.init中设置的attachStacktrace,代表需要追踪错误堆栈
  • isErrorEvent如果错误类型是ErrorEvent 会走eventFromStacktrace(computeStackTrace(exception))

      function isErrorEvent(wat) {
        return Object.prototype.toString.call(wat) === '[object ErrorEvent]';
      }
    复制代码
  • isDOMError代表DOMError(已经废弃),isDOMException代表DOMException,调用eventFromString(注意第二个参数syntheticException为null)

      function isDOMError(wat) {
        return Object.prototype.toString.call(wat) === '[object DOMError]';
      }
      
       function isDOMException(wat) {
        return Object.prototype.toString.call(wat) === '[object DOMException]';
      }
    复制代码
  • isError 里是Error 或者 Exception 或者DOMException会走这里,会走eventFromStacktrace(computeStackTrace(exception))

      function isError(wat) {
        switch (Object.prototype.toString.call(wat)) {
          case '[object Error]':
            return true;
          case '[object Exception]':
            return true;
          case '[object DOMException]':
            return true;
          default:
            return isInstanceOf(wat, Error);
        }
      }
    复制代码
  • isPlainObject或者isEvent针对普通消息,会走eventFromPlainObject(注意第二个参数syntheticException为null)

      function isEvent(wat) {
        return typeof Event !== 'undefined' && isInstanceOf(wat, Event);
      }
    
      function isPlainObject(wat) {
        return Object.prototype.toString.call(wat) === '[object Object]';
      }
    复制代码
  • 其他的就当成简单消息处理了

其实总的看起来,处理eventFromStacktrace,eventFromPlainObject,eventFromString 都是拿到错误消息进行更进一步的数据处理。

其中computeStackTrace是抹平差异,生成错误堆栈的关键

(1) 重点:computeStackTrace 获取错误堆栈

computeStackTrace基于 TraceKit 中的处理方法进行一些改造,主要是抹平各个浏览器间对于错误堆栈的差异

  function computeStackTrace(ex) {
      let stack = null;
      let popSize = 0;
  	// ...
      try {
          stack = computeStackTraceFromStackProp(ex);
          if (stack) {
              return popFrames(stack, popSize);
          }
      }
      catch (e) {
          // no-empty
      }
      return {
          message: extractMessage(ex),
          name: ex && ex.name,
          stack: [],
          failed: true,
      };
  }
复制代码

computeStackTraceFromStackProp

抹平浏览器差异的具体实现

  function computeStackTraceFromStackProp(ex) {
    if (!ex || !ex.stack) {
      return null;
    }
    const stack = [];
    const lines = ex.stack.split('\n');
    let isEval;
    let submatch;
    let parts;
    let element;
    for (let i = 0; i < lines.length; ++i) {
      if ((parts = chrome.exec(lines[i]))) {
        const isNative = parts[2] && parts[2].indexOf('native') === 0; // start of line
        isEval = parts[2] && parts[2].indexOf('eval') === 0; // start of line
        if (isEval && (submatch = chromeEval.exec(parts[2]))) {
          // throw out eval line/column and use top-most line/column number
          parts[2] = submatch[1]; // url
          parts[3] = submatch[2]; // line
          parts[4] = submatch[3]; // column
        }
        element = {
          // working with the regexp above is super painful. it is quite a hack, but just stripping the `address at `
          // prefix here seems like the quickest solution for now.
          url: parts[2] && parts[2].indexOf('address at ') === 0 ? parts[2].substr('address at '.length) : parts[2],
          func: parts[1] || UNKNOWN_FUNCTION,
          args: isNative ? [parts[2]] : [],
          line: parts[3] ? +parts[3] : null,
          column: parts[4] ? +parts[4] : null,
        };
      } else if ((parts = winjs.exec(lines[i]))) {
        element = {
          url: parts[2],
          func: parts[1] || UNKNOWN_FUNCTION,
          args: [],
          line: +parts[3],
          column: parts[4] ? +parts[4] : null,
        };
      } else if ((parts = gecko.exec(lines[i]))) {
        isEval = parts[3] && parts[3].indexOf(' > eval') > -1;
        if (isEval && (submatch = geckoEval.exec(parts[3]))) {
          // throw out eval line/column and use top-most line number
          parts[1] = parts[1] || `eval`;
          parts[3] = submatch[1];
          parts[4] = submatch[2];
          parts[5] = ''; // no column when eval
        } else if (i === 0 && !parts[5] && ex.columnNumber !== void 0) {
          // FireFox uses this awesome columnNumber property for its top frame
          // Also note, Firefox's column number is 0-based and everything else expects 1-based,
          // so adding 1
          // NOTE: this hack doesn't work if top-most frame is eval
          stack[0].column = ex.columnNumber + 1;
        }
        element = {
          url: parts[3],
          func: parts[1] || UNKNOWN_FUNCTION,
          args: parts[2] ? parts[2].split(',') : [],
          line: parts[4] ? +parts[4] : null,
          column: parts[5] ? +parts[5] : null,
        };
      } else {
        continue;
      }
      if (!element.func && element.line) {
        element.func = UNKNOWN_FUNCTION;
      }
      stack.push(element);
    }
    if (!stack.length) {
      return null;
    }
    return {
      message: extractMessage(ex),
      name: ex.name,
      stack,
    };
  }
复制代码

分析:

  • 通过ex.stack可以获取当前的错误堆栈。

    这里我举一个错误例子方便理解。获取到的错误堆栈为:

    Error: externalLibrary method broken: 1610359373199
    at externalLibrary (https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:2:9)
    at https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:5:1"
    复制代码
  • 通过ex.stack.split('\n’)转成数组,这里需要对每一个错误栈遍历是因为比如:'Error: externalLibrary method broken: 1610359373199'这种浏览器都是没有区别的可以直接跳过

    [
    	'Error: externalLibrary method broken: 1610359373199',
      'at externalLibrary  		 (https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:2:9)',
      'at https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js:5:1"
      '
    ]
    复制代码
  • 然后通过正则对当前的错误判断属于哪种浏览器内核,然后做不同的处理,抹平差异

  • 最后返回stack是带有args,line,func,url的数据格式

    args: []
    column: 9
    func: "externalLibrary"
    line: 2
    url: "https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js"
    复制代码
(2)eventFromStacktrace、eventFromString和 eventFromPlainObject

这三个统一讲解是因为原理都是一样的,都是拿到错误堆栈等信息进行进一步处理,

注意这里的eventFromString和eventFromPlainObject传入的syntheticException都是null所以是不会产生错误堆栈

来看看他们的源码:

  
  function eventFromStacktrace(stacktrace) {
    const exception = exceptionFromStacktrace(stacktrace);
    return {
      exception: {
        values: [exception],
      },
    };
  }
--------------------------------------------------------------------------------------------
  function eventFromString(input, syntheticException, options = {}) 	{
    const event = {
      message: input,
    };
    if (options.attachStacktrace && syntheticException) {
      const stacktrace = computeStackTrace(syntheticException);
      const frames = prepareFramesForEvent(stacktrace.stack);
      event.stacktrace = {
        frames,
      };
    }
    return event;
  }
-------------------------------------------------------------------------------------------
function eventFromPlainObject(exception, syntheticException, rejection) {
    const event = {
      exception: {
        values: [
          {
            type: isEvent(exception) ? exception.constructor.name : rejection ? 'UnhandledRejection' : 'Error',
            value: `Non-Error ${
              rejection ? 'promise rejection' : 'exception'
            } captured with keys: ${extractExceptionKeysForMessage(exception)}`,
          },
        ],
      },
      extra: {
        __serialized__: normalizeToSize(exception),
      },
    };
    if (syntheticException) {
      const stacktrace = computeStackTrace(syntheticException);
      const frames = prepareFramesForEvent(stacktrace.stack);
      event.stacktrace = {
        frames,
      };
    }
    return event;
  }

复制代码

分析:

  • computeStackTrace 就是我们上面讲过的抹平差异,获取错误堆栈

  • exception包含了错误类型和信息

  • exceptionFromStacktrace实际上是调用prepareFramesForEvent获取错误堆栈

  • 看看最终返回的数据吧

      exception:  {
          stacktrace: [
            {
              colno: 1,
              filename:
              'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js',
              function: '?',
              in_app: true,
              lineno: 5,
            },
            {
              colno: 9,
              filename:
                'https://rawgit.com/kamilogorek/cfbe9f92196c6c610535e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js',
              function: 'externalLibrary',
              in_app: true,
              lineno: 2,
            },
          ];
          type: 'Error';
          value: 'externalLibrary method broken: 1610364003791';
        }
    复制代码
(3) 小结

eventFromUnknownInput 虽然代码很多,但是很清晰。

根据错误消息的类型去走eventFromStacktrace、eventFromString和 eventFromPlainObject其中的一个方法。像错误类型为DOMError、DOMException、普通对象的此时是没有错误堆栈的消息的,而其他的会通过computeStackTrace去抹平不同浏览器的差异,获取错误堆栈,最后对错误数据处理返回统一的结构

4.4 _enhanceEventWithInitialFrame

    _enhanceEventWithInitialFrame(event, url, line, column) {
      event.exception = event.exception || {};
      event.exception.values = event.exception.values || [];
      event.exception.values[0] = event.exception.values[0] || {};
      event.exception.values[0].stacktrace = event.exception.values[0].stacktrace || {};
      event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames || [];
      const colno = isNaN(parseInt(column, 10)) ? undefined : column;
      const lineno = isNaN(parseInt(line, 10)) ? undefined : line;
      const filename = isString(url) && url.length > 0 ? url : getLocationHref();
      if (event.exception.values[0].stacktrace.frames.length === 0) {
        event.exception.values[0].stacktrace.frames.push({
          colno,
          filename,
          function: '?',
          in_app: true,
          lineno,
        });
      }
      return event;
    }
  }
复制代码

分析:

  • 通过eventFromUnknownInput拿到event,url,line,colum后调用_enhanceEventWithInitialFrame
  • _enhanceEventWithInitialFrame确保exception,values,stacktrace,frames设置有默认值

4.5 _eventFromRejectionWithPrimitive

    _eventFromRejectionWithPrimitive(reason) {
      return {
        exception: {
          values: [
            {
              type: 'UnhandledRejection',
              // String() is needed because the Primitive type includes symbols (which can't be automatically stringified)
              value: `Non-Error promise rejection captured with value: ${String(reason)}`,
            },
          ],
        },
      };
    }
复制代码

分析:

  • 代码很简单就是返回了默认格式

4.6 _eventFromIncompleteOnError

this._eventFromIncompleteOnError(data.msg, data.url, data.line, data.column)
--------------------------------------------------------------------------------------
    /**
     * This function creates a stack from an old, error-less onerror handler.
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    _eventFromIncompleteOnError(msg, url, line, column) {
      const ERROR_TYPES_RE = /^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/i;
      // If 'message' is ErrorEvent, get real message from inside
      let message = isErrorEvent(msg) ? msg.message : msg;
      let name;
      if (isString(message)) {
        const groups = message.match(ERROR_TYPES_RE);
        if (groups) {
          name = groups[1];
          message = groups[2];
        }
      }
      const event = {
        exception: {
          values: [
            {
              type: name || 'Error',
              value: message,
            },
          ],
        },
      };
      return this._enhanceEventWithInitialFrame(event, url, line, column);
    }
--------------------------------------------------------------------------------------
  _enhanceEventWithInitialFrame(event, url, line, column) {
      event.exception = event.exception || {};
      event.exception.values = event.exception.values || [];
      event.exception.values[0] = event.exception.values[0] || {};
      event.exception.values[0].stacktrace = event.exception.values[0].stacktrace || {};
      event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames || [];
      const colno = isNaN(parseInt(column, 10)) ? undefined : column;
      const lineno = isNaN(parseInt(line, 10)) ? undefined : line;
      const filename = isString(url) && url.length > 0 ? url : getLocationHref();
      if (event.exception.values[0].stacktrace.frames.length === 0) {
        event.exception.values[0].stacktrace.frames.push({
          colno,
          filename,
          function: '?',
          in_app: true,
          lineno,
        });
      }
      return event;
    }
  }
复制代码

分析:

  • _eventFromIncompleteOnError 对于基本类型的错误,可以直接获取错误信息,所以参数里就直接传入 url, line, column
  • _eventFromIncompleteOnError 就是添加错误类型和信息
  • _enhanceEventWithInitialFrame就是添加错误堆栈

到此,window.onerror和window.unhandrerejection 对于错误的不同处理已经研究完毕,接下来就是最后对错误数据进行统一处理。

4.7 addExceptionMechanis

addExceptionMechanism为event事件添加mechanism属性

比如上面的例子经过addExceptionMechanism处理后

addExceptionMechanism(event, {
  handled: false,
  type: 'onerror',
});
复制代码

得出如下数据格式:

 exception: {
      stacktrace: [
     	// ...
      ],
      type: 'Error',
      value: 'externalLibrary method broken: 1610364003791',
      mechanism:{
      	type:'onerror',
      	handled:false
      }
    }
复制代码

4.8 currentHub.captureEvent

到这里,已经把错误信息转换成我们想要的数据格式了。

currentHub.captureEvent 这边就是进行上报数据了,这一块内容将在下一篇文章玩转前端监控,全面解析Sentry源码(三)| 数据上报讲解。

4.9 补充:主动上报的错误信息如何处理

除了靠监听window.onerror和window.unhandledrejection,用户还可以采用captureException主动上报错误数据

接下来我们看看这与监听时处理数据有什么不同

 function captureException(exception, hint, scope) {
		// ...
      this._process(
        this._getBackend()
          .eventFromException(exception, hint)
         // ...
      );
      return eventId;
    }
----------------------------------------------------------------------------------
  function eventFromException(options, exception, hint) {
    const syntheticException = (hint && hint.syntheticException) || undefined;
    const event = eventFromUnknownInput(exception, syntheticException, {
      attachStacktrace: options.attachStacktrace,
    });
    addExceptionMechanism(event, {
      handled: true,
      type: 'generic',
    });
  	// ...
  }
复制代码

分析:

  • 可以看到主动上报走的是eventFromException方法
  • eventFromException实际上就是调用4.3 eventFromUnknownInput
  • 注意,这里syntheticException不是undefind,是可以去追踪错误堆栈的

4.10 总结

​ 最后我们通过一张流程图来理一理当发生错误的时候,来看看Sentry都做了什么。

五、实战

这一章节主要通过几个例子,让大家直观地看到各种类型的错误经过Sentry处理后会对应返回的数据类型

Sentry.init配置

Sentry.init({
  dsn: '..',
  attachStacktrace: true,
  debug: true,
  enabled: true,
  release: '..',
});
复制代码

5.1 window.error | 错误类型是基本类型

这里会走window.onerror中的4.6 _eventFromIncompleteOnError方法去处理数据

测试用例:

    <button id="error-string">window.error | string Error</button>
  <script>
  document.querySelector('#error-string').addEventListener('click', () => {
    const script = document.createElement('script');
    script.src = './errorString.js';
    document.body.appendChild(script);
  });
  </script>
复制代码

errorString.js:

function externalLibrary(date) {
  throw `string Error${date}`;
}

externalLibrary(Date.now());

复制代码

展示结果:

对应错误数据:

      exception: {
        values: {
          mechanism: {
            handled: false,
            type: 'onerror',
          },

          stacktrace: {
            frames: [
              {
                colno: 3,
                filename: 'http://127.0.0.1:5500/packages/browser/examples/errorString.js',
                function: '?',
                in_app: true,
                lineno: 2,
              },
            ],
          },
          type: 'Error',
          value: 'string Error1610594068361',
        },
      },
复制代码

5.2 window.error | 错误类型是引用类型

这里会走window.onerror中的 4.4 _enhanceEventWithInitialFrame方法去处理数据,由于走的eventFromUnknownInput中的eventFromPlainObject,所以这里面是没有错误栈的消息的。

测试用例:

  <button id="error-throw-new-error">window.error | throw new Error</button>
  <script>
  document.querySelector('#error-throw-new-error').addEventListener('click', () => {
    console.log('click');
    const script = document.createElement('script');
    script.crossOrigin = 'anonymous';
    script.src =  'https://rawgit.com/kamilogorek/cfbe9f92196c6c61053b28b2d42e2f5d/raw/3aef6ff5e2fd2ad4a84205cd71e2496a445ebe1d/external-lib.js';
    document.body.appendChild(script);
  });
  </script>
复制代码

external-lib.js

function externalLibrary (date) {
  throw new Error(`externalLibrary method broken: ${date}`);
}
externalLibrary(Date.now());
复制代码

展示结果:

对应错误数据:

    {
      exception: {
        values: [
          {
            stacktrace: {
              mechanism: {
                handled: false,
                type: 'onunhandledrejection',
              },

              frames: [
                {
                  colno: 23,
                  filename: 'http://127.0.0.1:5500/packages/browser/examples/app.js',
                  function: '?',
                  in_app: true,
                  lineno: 171,
                },
              ],
            },
          },
        ];
      }
      type: 'Error';
      value: 'promise-throw-error';
    }
复制代码

5.3 window.error | 不上报错误

这里会走 4.1 shouldIgnoreOnError直接跳出,不进行错误上报,也就没有错误信息了

测试代码

    <button id="ignore-message">ignoreError message example</button>
   <script>
     document.querySelector('#ignore-message').addEventListener('click', () => {
    	fun()
  });
  </script>
复制代码

展示结果:

5.4 window.unhandledrejection |错误类型是基本类型

这里会走window.unhandledrejection中的4.5 _eventFromRejectionWithPrimitive方法去处理数据,这里面是没有错误栈的消息的。

测试代码

   <button id="unhandledrejection-string">window.unhandledrejection | string-error</button>
    <script>
  document.querySelector('#unhandledrejection-string').addEventListener('click', () => {
    new Promise(function(resolve, reject) {
      setTimeout(function() {
        return reject('oh string error');
      }, 200);
    });
  });
  </script>
复制代码

展示结果:

对应错误数据:

      exception: {
        values: [
          {
            mechanism: { handled: false, type: 'onunhandledrejection' },
          },
        ],
        type: 'UnhandledRejection',
        value: 'Non-Error promise rejection captured with value: oh string error',
        level: 'error',
      }
复制代码

5.5 window.unhandledrejection |错误类型是引用类型

这里会走window.unhandledrejection中的 4.3 重点:eventFromUnknownInput中的eventFromPlainObject,所以这里面是没有错误栈的消息的。

测试代码

    <button id="unhandledrejection-plainObject">window.unhandledrejection | plainObject</button>
    <script>
  document.querySelector('#unhandledrejection-plainObject').addEventListener('click', () => {
    new Promise(function(resolve, reject) {
      setTimeout(function() {
        const obj = {
          msg: 'plainObject',
          testObj: {
            message: '这是个测试数据',
          },
          date: new Date(),
          reg: new RegExp(),
          testFun: () => {
            console.log('testFun');
          },
        };
        return reject(obj);
      }, 200);
    });
  });
  </script>
复制代码

展示结果:

对应错误数据:

      exception: {
        values: [
          {
            type: 'UnhandledRejection',
            value: '"Non-Error promise rejection captured with keys: msg, testFun, testObj"',
          },
        ],

        extra: {
          __serialized__: {
            msg: 'plainObject',
            testFun: '[Function: testFun]',
            testObj: {
              message: '这是个测试数据',
            },
          },
        },
复制代码

5.5 window.unhandledrejection |错误类型是引用类型并且是DOMException

这里会走window.unhandledrejection中的 4.3 重点:eventFromUnknownInput方法去处理数据,由于走的eventFromUnknownInput中的eventFromString,所以这里面是没有错误栈的消息的。

测试代码

<button id="DOMException">DOMException</button>
<script>
  document.querySelector('#DOMException').addEventListener('click', () => {
    screen.orientation.lock('portrait');
  });
</script>
复制代码

展示结果:

对应错误数据:

      exception: {
        values: [
          {
            type: 'Error',
            value: 'NotSupportedError: screen.orientation.lock() is not available on this device.',
          },
        ],
      },
      message: 'NotSupportedError: screen.orientation.lock() is not available on this device.',
      tags: {
        'DOMException.code': '9',
      }
复制代码

5.5 主动上报

这里走的是主动上报的错误信息如何处理

测试代码

<button id="DOMException">DOMException</button>
<script>
  document.querySelector('#capture-exception').addEventListener('click', () => {
    Sentry.captureException(new Error(`captureException call no. ${Date.now()}`));
  });
</script>
复制代码

展示结果:

控制台没有任何错误,因为Sentry.captureException是向客户端发送一个错误数据

对应错误数据:

     exception: {
        event_id: '0e7c9eb8fa3c42b786543317943e9d0d',
        exception: {
          values: [
            {
              mechanism: {
                handled: true,
                type: 'generic',
              },
              stacktrace: {
                frames: [
                  {
                    colno: 29,
                    filename: 'http://127.0.0.1:5500/packages/browser/examples/app.js',
                    function: 'HTMLButtonElement.<anonymous>',
                    in_app: true,
                    lineno: 142,
                  },
                ],
              },
            },
          ],
          type: 'Error',
          value: 'captureException call no. 1610599823126',
        },
      }
复制代码

六、总结

Sentry在初始化的时候对window.onerror和window.unhandledrejection进行劫持,当错误发生的时候,为了抹平各个浏览器内核对于错误堆栈返回的信息不同用computeStackTrace进行抹平差异获取错误堆栈。对于不同类型的错误消息都有自己对应的处理方式,最后输出统一的错误数据。

接着是本文没有讲解的内容:Sentr之后的工作就是获取非错误的数据,如: user-agent浏览器信息系统信息自定义信息等信息,然后交给Sentry的生命周期函数,最后在把数据发送到Sentry服务端,进行错误信息展示。

七、参考资料

文章分类
前端
文章标签