技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

前端捕获异常分为全局捕获和单点捕获。

  • 全局捕获代码集中,易于管理;
  • 单点捕获作为补充,对某些特殊情况进行捕获,但分散,不利于管理。
  1. 全局捕获

    • 通过全局的接口,将捕获代码集中写在一个地方,可以利用的接口有:

      • window.addEventListener(‘error’) / window.addEventListener(“unhandledrejection”) / document.addEventListener(‘click’)
    • 框架级别的全局监听,

      • 例如aixos中使用interceptor进行拦截,
      • vue、react都有自己的错误采集接口
    • 通过对全局函数进行封装包裹,实现在在调用该函数时自动捕获异常

    • 对实例方法重写(Patch),在原有功能基础上包裹一层,

      • 例如对setTimeout进行重写,在使用方法不变的情况下也可以异常捕获
  2. 单点捕获

    • 在业务代码中对单个代码块进行包裹,或在逻辑流程中打点,实现有针对性的异常捕获:
    • try…catch
    • 专门写一个函数来收集异常信息,在异常发生时,调用该函数
    • 专门写一个函数来包裹其他函数,得到一个新函数,该新函数运行结果和原函数一模一样,只是在发生异常时可以捕获异常

# 3.2 详细讲解

  • window.onerror
    • 监控JavaScript运行时错误(包括语法错误)和 资源加载错误

window.onerror = function(message, source, lineno, colno, error) { ... }
window.addEventListener('error', function(event) { ... }, true)
// 函数参数:
    // message:错误信息(字符串)。可用于HTML onerror=""处理程序中的event。
    // source:发生错误的脚本URL(字符串)
    // lineno:发生错误的行号(数字)
    // colno:发生错误的列号(数字)
    // error:Error对象(对象

大家可以看到 JS 错误监控里面有个 window.onError,
又用了 window.addEventLisenter('error'),
其实两者并不能互相代替。
window.onError 是一个标准的错误捕获接口,它可以拿到对应的这种 JS 错误;
window.addEventLisenter('error')也可以捕获到错误,
但是它拿到的 JS 报错堆栈往往是不完整的。
同时 window.onError 无法获取到资源加载失败的一个情况,
必须使用 window.addEventLisenter('error')来捕获资源加载失败的情况。
  • Promise
    • Promise的话主要是unhandledrejection事件,也就是未被catchreject状态的promise

window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
  • setTimeout、setInterval、requestAnimationFrame
    • 其实就是通过代理的方式把原来的方法拦截一下在调用真实的方法之前做一些自己的事情

const prevSetTimeout = window.setTimeout;

window.setTimeout = function(callback, timeout) {
  const self = this;
  return prevSetTimeout(function() {
    try {
      callback.call(this);
    } catch (e) {
      // 捕获到详细的错误,在这里处理日志上报等了逻辑
      // ...
      throw e;
    }
  }, timeout);
}
  • VueVue.config.errorHandler
    • 跟上面的同理

// sentry中对Vue errorHandler的处理
function vuePlugin(Raven, Vue) {
  var _oldOnError = Vue.config.errorHandler;
  Vue.config.errorHandler = function VueErrorHandler(error, vm, info) {
    // 上报
    Raven.captureException(error, {
      extra: metaData
    });

    if (typeof _oldOnError === 'function') {
      // 为什么这么做?
      _oldOnError.call(this, error, vm, info);
    }
  };
}
module.exports = vuePlugin;
  • ReactErrorBoundary
    • ErrorBoundary的定义:如果一个class组件中定义了static getDerivedStateFromError()componentDidCatch()这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用static getDerivedStateFromError()渲染备用 UI ,使用componentDidCatch()打印错误信息

// ErrorBoundary的示例
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    // 在这里可以做异常的上报
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
}

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>
  • 那么Sentry是怎么实现的呢?

// ts声明的类型,可以看到sentry大概实现的方法
/**
 * A ErrorBoundary component that logs errors to Sentry.
 * Requires React >= 16
 */
declare class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
    state: ErrorBoundaryState;
    componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void;
    componentDidMount(): void;
    componentWillUnmount(): void;
    resetErrorBoundary: () => void;
    render(): React.ReactNode;
}

// 真实上报的地方
ErrorBoundary.prototype.componentDidCatch = function (error, _a) {
  var _this = this;
  var componentStack = _a.componentStack;
  // 获取到配置的props
  var _b = this.props, beforeCapture = _b.beforeCapture, onError = _b.onError, showDialog = _b.showDialog, dialogOptions = _b.dialogOptions;
  withScope(function (scope) {
    // 上报之前做一些处理,相当于axios的请求拦截器
    if (beforeCapture) {
      beforeCapture(scope, error, componentStack);
    }
    // 上报
    var eventId = captureException(error, { contexts: { react: { componentStack: componentStack } } });
    // 开发者的回调
    if (onError) {
      onError(error, componentStack, eventId);
    }
    // 是否显示sentry的错误反馈组件(也是一种收集错误的方式)
    if (showDialog) {
      showReportDialog(__assign(__assign({}, dialogOptions), { eventId: eventId }));
    }
    // componentDidCatch is used over getDerivedStateFromError
    // so that componentStack is accessible through state.
    _this.setState({ error: error, componentStack: componentStack, eventId: eventId });
  });
};
  • 请求

    • XHR通过重写(拦截)send和open
    • fetch通过拦截整个方法(需要讨论,reject的情况)
    • axios通过请求/响应拦截器 注意:sentry支持自动收集和手动收集两种错误收集方法,但是还不能捕捉到异步操作、接口请求中的错误,比如接口返回404、500等信息,此时我们可以通过Sentry.caputureException()进行主动上报。
    1. xhr的实现
    
    function fill(source, name, replacementFactory) {
     var original = source[name];
     var wrapped = replacementFactory(original);
     source[name] = wrapped;
    }
    // xhr
    function instrumentXHR(): void {
       // 保存真实的xhr的原型
       const xhrproto = XMLHttpRequest.prototype;
       // 拦截open方法
       fill(xhrproto, 'open', function (originalOpen: () => void): () => void {
           return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
             const xhr = this;
             const onreadystatechangeHandler = function (): void {
               if (xhr.readyState === 4) {
                 if (xhr.__sentry_xhr__) {
                     xhr.__sentry_xhr__.status_code = xhr.status;
                   }
                 // // 上报sentry
                 triggerHandlers('xhr', {
                   args,
                   endTimestamp: Date.now(),
                   startTimestamp: Date.now(),
                   xhr,
                 });
               }
             };
    
         if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
           // 拦截onreadystatechange方法
           fill(xhr, 'onreadystatechange', function (original: WrappedFunction): Function {
             return function (...readyStateArgs: any[]): void {
               onreadystatechangeHandler();
               // 返回原来的方法
               return original.apply(xhr, readyStateArgs);
             };
           });
         } else {
           xhr.addEventListener('readystatechange', onreadystatechangeHandler);
         }
         // 调用原来的方法
         return originalOpen.apply(xhr, args);
       };
    });
         // fill其实就是拦截的一个封装originalSend就是原来的send方法
       fill(xhrproto, 'send', function (originalSend: () => void): () => void {
           return function (this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
             // 上报sentry
             triggerHandlers('xhr', {
               args,
               startTimestamp: Date.now(),
               xhr: this,
             });
             // 返回原来方法
             return originalSend.apply(this, args);
           };
       });
    }
    
    • Fetch
    
    // 重写fetch
    function instrumentFetch() {
      if (!supportsNativeFetch()) {
        return;
      }
      fill(global$2, 'fetch', function (originalFetch) {
        return function () {
          var args = [];
          for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
          }
          var handlerData = {
            args: args,
            fetchData: {
              method: getFetchMethod(args),
              url: getFetchUrl(args),
            },
            startTimestamp: Date.now(),
          };
          triggerHandlers('fetch', __assign({}, handlerData));
          // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
          return originalFetch.apply(global$2, args).then(function (response) {
            triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), response: response }));
            return response;
          }, function (error) {
            triggerHandlers('fetch', __assign(__assign({}, handlerData), { endTimestamp: Date.now(), error: error }));
            throw error;
          });
        };
      });
    }
    
    • console.xxx
    
    function instrumentConsole() {
      if (!('console' in global$2)) {
        return;
      }
      ['debug', 'info', 'warn', 'error', 'log', 'assert'].forEach(function (level) {
        if (!(level in global$2.console)) {
          return;
        }
        fill(global$2.console, level, function (originalConsoleLevel) {
          return function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
              args[_i] = arguments[_i];
            }
            // 上报sentry
            triggerHandlers('console', { args: args, level: level });
            // this fails for some browsers. :(
            if (originalConsoleLevel) {
              Function.prototype.apply.call(originalConsoleLevel, global$2.console, args);
            }
          };
        });
      });
    }
    

# 4. 性能监控(建议阅读Web Vita (opens new window)

【未经作者允许禁止转载】 Last Updated: 2/4/2024, 6:06:40 AM