异常监控-异常捕获

2020-07-28 11:15:282021-02-20 17:14:19

异常分类

  • 编译时异常/运行时异常

着重处理运行时异常

要处理的异常

  • js运行时异常
  • 静态资源加载异常
  • Promise异常
  • ajax请求异常
  • 崩溃和卡顿

捕获异常

普通前端项目

// monitor.ts

interface ConfigI {
  reportUrl: string;
}

interface BaseinfoI {
  title: string;
  location: string;
  message: string;
  kind: string;
  type: string;
  errorType: string;
}

interface JSRunTimeErrorEventI extends BaseinfoI {
  filename: string;
  position: string;
  stack: string;
  selector: string;
}

interface AssetsErrorI extends BaseinfoI {
  url: string;
  nodeName: string;
}

interface AjaxErrorEventI extends BaseinfoI {
  response: string;
  status: number;
  method: string;
  url: string;
}

interface PromiseErrorI extends BaseinfoI {
  message: string;
  stack: string;
}

const config: ConfigI = { reportUrl: '/' };

function getLastEvent(): undefined | Event {
  let lastEvent;
  ['click', 'touchstart', 'mousedown', 'keydown', 'mouseover'].forEach((eventType) => document.addEventListener(eventType, (event) => {
    lastEvent = event;
  }, {
    capture: true,
    passive: true,
  }));
  return lastEvent;
}

function getSelector(path: Array<EventTarget>) {
  return '';
}

function report<T>(data: { type: 'JSRunTimeErrorI' | 'PromiseErrorT' | 'AssetsErrorI' | 'AjaxErrorEventI'; info: T }) {
  const image = new Image();
  image.src = `${config.reportUrl}?error=${JSON.stringify(data.info)}`;
}

function getCommonInfoFromEvent(event?: Event) {
  return {
    title: document.title.replace(/&/, ''),
    location: window.location.href.replace(/&/, ''),
    kind: 'stability',
    type: 'error',
  };
}

function getLines(stack: string | undefined) {
  return stack?.split('\n').slice(1).map((item) => item.replace(/^\s+at\s+/g, '')).join('') || '';
}

// Promise异常
function promiseError() {
  window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
    event.preventDefault();
    let message = '';
    let stack = '';
    message = event.reason;
    const { reason } = event;
    if (reason instanceof Error) {
      message = reason.message;
      stack = getLines(reason.stack);
    }

    report<PromiseErrorI>({ type: 'PromiseErrorT', info: { errorType: event.type, message, stack, ...getCommonInfoFromEvent() } });
    return true;
  }, true);
}

// 静态资源加载异常&JSRuntime异常
function assetsError() {
  window.addEventListener('error', (event: ErrorEvent) => {
    const { target } = event;
    console.log(event);
    const isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
    if (!isElementTarget) {
      const { message, filename, lineno, colno, error, type } = event;
      const position = `${lineno}:${colno}`;
      const stack = getLines(error instanceof Error ? error.stack : '');
      const lastEvent = getLastEvent();
      const selector = lastEvent ? getSelector(lastEvent.composedPath()) : '';
      report<JSRunTimeErrorEventI>({
        type: 'JSRunTimeErrorI',
        info: { ...getCommonInfoFromEvent(), message, errorType: type, filename, position, stack, selector },
      });
    } else {
      let url = '';
      let nodeName = '';
      if (target instanceof HTMLImageElement || target instanceof HTMLScriptElement) {
        url = target.src;
        nodeName = target.nodeName;
      }
      if (target instanceof HTMLLinkElement) {
        url = target.href;
        nodeName = target.nodeName;
      }

      report<AssetsErrorI>({ type: 'AssetsErrorI',
        info: {
          url,
          errorType: event.type,
          nodeName,
          message: '',
          ...getCommonInfoFromEvent() } });
    }
    return true;
  }, true);
}

// ajax请求异常
function ajaxError() {
  const { protocol } = window.location;
  if (protocol === 'file:') return;
  if (!window.XMLHttpRequest) return;
  const xmlReq = window.XMLHttpRequest;
  const oldSend = xmlReq.prototype.send;
  const oldOpen = xmlReq.prototype.open;
  const oldArgs = { method: '', url: '' };
  function handleEvent(event: any) {
    try {
      if (event && event.currentTarget && event.currentTarget.status !== 200) {
        const { response, status, statusText } = event.currentTarget;
        const { method, url } = oldArgs;
        console.log(event);

        report<AjaxErrorEventI>({
          type: 'AjaxErrorEventI',
          info: {
            response: JSON.parse(response),
            status,
            method,
            url,
            message: statusText,
            errorType: event.type,
            ...getCommonInfoFromEvent(),
          },
        });
      }
    } catch (e) {
      console.log(`Tool's error: ${e}`);
    }
  }
  xmlReq.prototype.send = function (...args) {
    this.addEventListener('error', handleEvent);
    this.addEventListener('load', handleEvent);
    this.addEventListener('abort', handleEvent);
    return oldSend.apply(this, args);
  };
  xmlReq.prototype.open = function (...args: any) {
    const [method, url] = args;
    Object.assign(oldArgs, { method, url });
    oldOpen.apply(this, args);
  };
}

function init(_config: ConfigI) {
  Object.assign(config, _config);
  promiseError();
  assetsError();
  ajaxError();
}

export default init;

基于Vue.js的项目

针对于Vue.js项目,虽然官方提供有Vue.config.errorHandler来拦截内部错误(生命周期、事件绑定等)。但处理能力有限,无法处理js运行时异步异常(如setTimeout)和资源加载异常等,因此需要使用mointor.js + Vue.config.errorHandler来共同处理

基于React.js的项目

  1. 引入monitor.ts

  2. 添加MErrorBoundary组件用户出现异常后优雅降级显示和收集出错组件

import React from 'react';

class MErrorBoundary extends React.Component<any, any> {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError() {
    console.log('MErrorBoundary');
    return { hasError: true };
  }

  componentDidCatch(error: any, errorInfo: any) {
    // logErrorToMyService(error, errorInfo);
  }

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

    return this.props.children;
  }
}

export default MErrorBoundary;

白屏和卡顿

// todo