最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    正文概述 掘金(TTtttt)   2021-01-17   1182

    一、前言

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

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

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

    • 搞懂Sentry初始化
    • Sentry如何处理错误数据(本文)
    • Sentry拿到错误数据后,又是如何上报呢?
    • 计划:Sentry如何集成面包屑Breadcrumbs
    • 计划:Sentry如何处理session
    • ...

    这里给觉得看源码很困难的小伙伴一些建议:

    • 推荐先看一些文章或者官网文档,并且相应的api要熟悉。然后根据主线走,支线可以大概了解是做什么的,别深究。
    • 有时候可能分不清主次,这个时候可以画流程图,这也是我快速了解源码的方法。你画着画着,原本一开始看不懂的源码,可能在后续的某个地方就关联了起来。
    • 最后就是要去多思考,比如:从源码中我学到了哪些优秀的写法,用到了什么设计模式,我如何应用到实际项目中,碰到bug是不是能够通过自己知道的运行流程快速定位到bug大概可能出现的位置等等。

    而关于Sentry源码如何调试大家可以按照如下:

    • git clone git@github.com:getsentry/sentry-javascript.git
    • 进入到 packages/browser 进行npm i 下载依赖
    • 进入到 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 = 'https://docs.sentry.io/error?error=error'

    三、如何监听错误

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

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    关于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如何处理错误数据

    五、实战

    这一章节主要通过几个例子,让大家直观地看到各种类型的错误经过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());
    
    

    展示结果: 玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    对应错误数据:

          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());
    

    展示结果:

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    对应错误数据:

        {
          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>
    

    展示结果:

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    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>
    

    展示结果:

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    对应错误数据:

          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>
    

    展示结果:

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    对应错误数据:

          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>
    

    展示结果:

    玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    对应错误数据:

          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服务端,进行错误信息展示。

    七、参考资料

    • Sentry官网
    • Sentry仓库
    • Error MDN
    • ErrorEvent MDN
    • DOMException MDN
    • 前端异常监控-看这篇就够了
    • 聊聊前端监控——错误监控篇

    起源地下载网 » 玩转前端监控,全面解析Sentry源码(二)| Sentry如何处理错误数据

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元