51Testing软件测试论坛

标题: Sentry如何自动捕获前端应用异常原理? [打印本页]

作者: 草帽路飞UU    时间: 2022-12-13 16:05
标题: Sentry如何自动捕获前端应用异常原理?
本帖最后由 草帽路飞UU 于 2022-12-13 16:06 编辑

常见的前端异常及其捕获方式



  前端异常通常可以分为以下几种类型:

  ·js 代码执行时异常;


  · promise 类型异常;


  · 资源加载类型异常;


  · 网络请求类型异常;


  · 跨域脚本执行异常;


  · 不同类型的异常,捕获方式不同。


  js 代码执行时异常


  js 代码执行异常,是我们经常遇到异常。这一类型的异常,又可以具体细分为:

  · Error,最基本的错误类型,其他的错误类型都继承自该类型。通过 Error,我们可以自定义 Error 类型。


  · RangeError: 范围错误。当出现堆栈溢出(递归没有终止条件)、数值超出范围(new Array 传入负数或者一个特别大的整数)情况时会抛出这个异常。


  · ReferenceError,引用错误。当一个不存在的对象被引用时发生的异常。


  · SyntaxError,语法错误。如变量以数字开头;花括号没有闭合等。


  · TypeError,类型错误。如把 number 当 str 使用。


  · URIError,向全局 URI 处理函数传递一个不合法的 URI 时,就会抛出这个异常。如使用 decodeURI('%')、decodeURIComponent('%')。


  · EvalError, 一个关于 eval 的异常,不会被 javascript 抛出。


  具体详见: Error - JavaScript - MDN Web Docs - Mozilla


  通常,我们会通过 try...catch 语句块来捕获这一类型异常。如果不使用 try...catch,我们也可以通过 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式进行全局捕


获。

  promise 类异常


  在使用 promise 时,如果 promise 被 reject 但没有做 catch 处理时,就会抛出 promise 类异常。

  Promise.reject(); // Uncaught (in promise) undefined

  promise 类型的异常无法被 try...catch 捕获,也无法被 window.onerror = callback 或者 window.addEventListener('error', callback) 的方式全局捕获。针对这一类型的异常, 我们需要通过


window.onrejectionhandled = callback 或者 window.addListener('rejectionhandled', callback) 的方式去全局捕获。


  静态资源加载类型异常

  如果我们页面的img、js、css 等资源链接失效,就会提示资源类型加载如异常。

  <img src="localhost:3000/data.png" /> // Get localhost:3000/data.png net::ERR_FILE_NOT_FOUND



  针对这一类的异常,我们可以通过 window.addEventListener('error', callback, true) 的方式进行全局捕获。


  这里要注意一点,使用 window.onerror = callback 的方式是无法捕获静态资源类异常的。


  原因是资源类型错误没有冒泡,只能在捕获阶段捕获,而 window.onerror 是通过在冒泡阶段捕获错误,对静态资源加载类型异常无效,所以只能借助 window.addEventListener('error', callback, true)


的方式捕获。

  接口请求类型异常


  在浏览器端发起一个接口请求时,如果请求的 url 的有问题,也会抛出异常。


  不同的请求方式,异常捕获方式也不相同:

  · 接口调用是通过 fetch 发起的


  我们可以通过 fetch(url).then(callback).catch(callback) 的方式去捕获异常。


  · 接口调用通过 xhr 实例发起


  如果是 xhr.open 方法执行时出现异常,可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。


  xhr.open('GET', "https://")  // Uncaught DOMException: Failed to execute 'open' on 'XMLHttpRequest': Invalid URL


  at ....

  如果是 xhr.send 方法执行时出现异常,可以通过 xhr.onerror 或者 xhr.addEventListener('error', callback) 的方式捕获异常。


  xhr.open('get', '/user/userInfo');


  xhr.send();  // send localhost:3000/user/userinfo net::ERR_FAILED




  跨域脚本执行异常


  当项目中引用的第三方脚本执行发生错误时,会抛出一类特殊的异常。这类型异常和我们刚才讲过的异常都不同,它的 msg 只有 'Script error' 信息,没有具体的行、列、类型信息。

  之以会这样,是因为浏览器的安全机制: 浏览器只允许同域下的脚本捕获具体异常信息,跨域脚本中的异常,不会报告错误的细节。


  针对这类型的异常,我们可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。

  如果我们想获取这类异常的详情,需要做以下两个操作:


  ·在发起请求的 script 标签上添加 crossorigin="anonymous";


  · 请求响应头中添加 Access-Control-Allow-Origin: *;


  这样就可以获取到跨域异常的细节信息了。


  Sentry 异常监控原理


  有效的异常监控需要哪些必备要素

  异常监控的核心作用就是通过上报的异常,帮开发人员及时发现线上问题并快速修复。

  要达到这个目的,异常监控需要做到以下 3 点:


  线上应用出现异常时,可以及时推送给开发人员,安排相关人员去处理。


  上报的异常,含有异常类型、发生异常的源文件及行列信息、异常的追踪栈信息等详细信息,可以帮助开发人员快速定位问题。


  可以获取发生异常的用户行为,帮助开发人员、测试人员重现问题和测试回归。


  这三点,分别对应异常自动推送、异常详情获取、用户行为获取。


  异常详情获取


  为了能自动捕获应用异常,Sentry 劫持覆写了 window.onerror 和 window.unhandledrejection 这两个 api。

  劫持覆写 window.onerror 的代码如下:


  oldErrorHandler = window.onerror;


  window.onerror = function (msg, url, line, column, error) {


      // 收集异常信息并上报


      triggerHandlers('error', {


          column: column,


          error: error,


          line: line,


          msg: msg,


          url: url,


      });


      if (oldErrorHandler) {


          return oldErrorHandler.apply(this, arguments);


      }


      return false;


  };




  劫持覆写 window.unhandledrejection 的代码如下:


  oldOnUnhandledRejectionHandler = window.onunhandledrejection;


  window.onunhandledrejection = function (e) {


      // 收集异常信息并上报



      triggerHandlers('unhandledrejection', e);

      if (oldOnUnhandledRejectionHandler) {


          return oldOnUnhandledRejectionHandler.apply(this, arguments);


      }


      return true;


  };




  虽然通过劫持覆写 window.onerror 和 window.unhandledrejection 已足以完成异常自动捕获,但为了能获取更详尽的异常信息, Sentry 在内部做了一些更细微的异常捕获。


  具体来说,就是 Sentry 内部对异常发生的特殊上下文,做了标记。这些特殊上下文包括: dom 节点事件回调、setTimeout / setInterval 回调、xhr 接口调用、requestAnimationFrame 回调等。


  举个 ,如果是 click 事件的 handler 中发生了异常, Sentry 会捕获这个异常,并将异常发生时的事件 name、dom 节点描述、handler 函数名等信息上报。

  具体处理逻辑如下:


  ·标记 setTimeout / setInterval / requestAnimationFrame


  · 为了标记 setTimeout / setInterval / requestAnimationFrame 类型的异常,Sentry 劫持覆写了原生的 setTimout / setInterval / requestAnimationFrame 方法。新的 setTimeout / setInterval /



requestAnimationFrame 方法调用时,会使用 try ... catch 语句块包裹 callback。

  具体实现如下:


  var originSetTimeout = window.setTimeout;


  window.setTimeout = function() {


      var args = [];


      for (var _i = 0; _i < arguments.length; _i++) {


          args[_i] = arguments[_i];


      }


      var originalCallback = args[0];


      // wrap$1 会对 setTimeout 的入参 callback 使用 try...catch 进行包装


      // 并在 catch 中上报异常


      args[0] = wrap$1(originalCallback, {


          mechanism: {


              data: { function: getFunctionName(original) },


              handled: true,


              // 异常的上下文是 setTimeout


              type: 'setTimeout',


          },


      });


      return original.apply(this, args);


  }




  ·当 callback 内部发生异常时,会被 catch 捕获,捕获的异常会标记 setTimeout。


  · 由于 setInterval、requestAnimationFrame 的劫持覆写逻辑和 setTimeout 基本一样,这里就不再重复说明了,感兴趣的小伙伴们可自行实现。


  · 标记 dom 事件 handler


  · 所有的 dom 节点都继承自 window.Node 对象,dom 对象的 addEventListener 方法来自 Node 的 prototype 对象。


  · 为了标记 dom 事件 handler,Sentry 对 Node.prototype.addEventListener 进行了劫持覆写。新的 addEventListener 方法调用时,同样会使用 try ... catch 语句块包裹传入的 handler。


  相关代码实现如下:


  function xxx() {


      var proto = window.Node.prototype;


      ...


      // 覆写 addEventListener 方法fill(proto, 'addEventListener', function (original) {



          return function (eventName, fn, options) {


              try {


                  if (typeof fn.handleEvent === 'function') {


                      // 使用 try...catch 包括 handle


                      fn.handleEvent = wrap$1(fn.handleEvent.bind(fn), {


                          mechanism: {


                              data: {


                                  function: 'handleEvent',


                                  handler: getFunctionName(fn),


                                  target: target,


                              },


                              handled: true,


                              type: 'instrument',


                          },


                      });


                  }


              }


              catch (err) {}


              return original.apply(this, [


                  eventName,


                  wrap$1(fn, {


                      mechanism: {


                          data: {


                              function: 'addEventListener',


                              handler: getFunctionName(fn),


                              target: target,


                          },




                          handled: true,


                          type: 'instrument',


                      },


                  }),


                  options,


              ]);


          };


      });


  }




  当 handler 内部发生异常时,会被 catch 捕获,捕获的异常会被标记 handleEvent, 并携带 event name、event target 等信息。


  其实,除了标记 dom 事件回调上下文,Sentry 还可以标记 Notification、WebSocket、XMLHttpRequest 等对象的事件回调上下文。可以这么说,只要一个对象有 addEventListener 方法并且可以被


劫持覆写,那么对应的回调上下文会可以被标记。

  标记 xhr 接口回调


  为了标记 xhr 接口回调,Sentry 先对 XMLHttpRequest.prototype.send 方法劫持覆写, 等 xhr 实例使用覆写以后的 send 方法时,再对 xhr 对象的 onload、onerror、onprogress、

onreadystatechange 方法进行了劫持覆写, 使用 try ... catch 语句块包裹传入的 callback。

  具体代码如下:


  fill(XMLHttpRequest.prototype, 'send', _wrapXHR);


  function _wrapXHR(originalSend) {


      return function () {


          var args = [];


          for (var _i = 0; _i < arguments.length; _i++) {


              args[_i] = arguments[_i];


          }


          var xhr = this;


          var xmlHttpRequestProps = ['onload', 'onerror', 'onprogress', 'onreadystatechange'];


          // 劫持覆写


          xmlHttpRequestProps.forEach(function (prop) {


              if (prop in xhr && typeof xhr[prop] === 'function') {


                  // 覆写


                  fill(xhr, prop, function (original) {


                      var wrapOptions = {


                          mechanism: {


                              data: {


                                  // 回调触发的阶段


                                  function: prop,


                                  handler: getFunctionName(original),


                              },


                              handled: true,


                              type: 'instrument',


                          },


                      };


                      var originalFunction = getOriginalFunction(original);


                      if (originalFunction) {


                          wrapOptions.mechanism.data.handler = getFunctionName(originalFunction);


                      }


                      return wrap$1(original, wrapOptions);


                  });


              }



          });

          return originalSend.apply(this, args);


      };




  当 callback 内部发生异常时,会被 catch 捕获,捕获的异常会被标记对应的请求阶段。


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









欢迎光临 51Testing软件测试论坛 (http://bbs.51testing.com/) Powered by Discuz! X3.2