前端错误监控-Sentry自动捕获前端应用异常原理

发表于:2022-12-08 09:43

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:前端餐厅ReTech    来源:今日头条

#
前端
  常见的前端异常及其捕获方式
  前端异常通常可以分为以下几种类型:
  ·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小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号