我理解的前端 error 监控

251 阅读1分钟

一、前言

前端错误类型:

  1. 同步JS 语法报错,如变量未定义 - VM212:1 Uncaught ReferenceError: xx is not defined

  2. Promise异常,如Promise.reject(xx) 未捕获的异常   - Uncaught (in promise) xx

  3. 异步代码执行过程中产生的错误, 如 setTimeout, promise.then(), try.catch 无法捕获异步

  4. html资源解析异常,如img, script 引入静态资源出错的错误,不会触发冒泡,只能触发onerror的捕获

  5. 接口响应异常,通过状态码拦截, 请求方法(原生XMLHttpRequest / axios / fetch)

二、 GlobalEventHandlers.onerror

该方法用来解决同步JS 语法报错跟 html 资源解析异常

介绍

Error事件的事件处理程序, 在各种目标对象的不同类型错误被触发:

  • 当JavaScript运行时错误(包括语法错误)发生时,window会触发一个ErrorEvent接口的error事件,并执行window.onerror()。
  • 同步语法错误通过try.catch进行拦截 / 异步错误 try.catch 拦截不到, 需要 onerror捕获, 但是还是会有错误打印
  • 当一项资源(如

语法

由于历史原因,window.onerrorelement.onerror接受不同的参数。

1、window.onerror

window.onerror = function(message, source, lineno, colno, error) { ... }

  • message:错误信息(字符串),可用于HTML onerror=""处理程序中的event。
  • source:发生错误的脚本URL(字符串)
  • lineno:发生错误的行号(数字)
  • colno:发生错误的列号(数字)
  • error:Error对象(对象)若该函数返回true,则阻止执行默认事件处理函数。

window.addEventListener('error', function(event) { ... })

2. element.onerror

element.onerror = function(event) { ... }

element.onerror使用单一Event参数的函数作为其处理函数。

三、 try...catch...

try...catch语句标记要尝试的语句块,并指定一个出现异常时抛出的响应。可以捕获同步异常
try块的代码可能会抛出三种异常:TypeErrorRangeErrorEvalError

语法

try {
   try_statements -- 执行的代码块
}
[catch (exception_var_1 if condition_1) { // 可以添加条件catch
   catch_statements_1
}]
...
[catch (exception_var_2) {
   catch_statements_2
}]
[finally {
   finally_statements
}]

四、 addEventListener('unhandledrejection', e => { ...} )

当 Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;

这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

unhandledrejection 继承自 PromiseRejectionEvent,而 PromiseRejectionEvent 又继承自 Event。

因此unhandledrejection 含有 PromiseRejectionEvent 和 Event 的属性和方法。

1. 基础上报语法

window.addEventListener("unhandledrejection", event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});
或者
window.onunhandledrejection = event => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
};

2. 阻止默认事件写法 (调用 preventDefault() 来取消该事件,从而防止该事件冒泡并由运行时的日志代码处理)

window.addEventListener('unhandledrejection', function (event) {
  // ...您的代码可以处理未处理的拒绝...

  // 防止默认处理(例如将错误输出到控制台)

  event.preventDefault();
});

五、 XMLHttpRequest() / axios / fetch 监控拦截状态码

1. XMLHttpRequest

class Request {
  constructor({ headers, timeout, success, fail }) {
    this.ajax = new XMLHttpRequest();
    this.headers = headers;
    this.timeout = timeout;
    this.success = success;
    this.fail = fail;
    this.url = "";
    this.params = {};
    this.init();
  }
  init() {
    // 2. 拦截状态码, 进行上报
    this.ajax.onreadystatechange = function () {
      const { readyState, status, responseText } = this;
      if (readyState === 4) {
        if (status === 200) {
          // 调用成功回调
          this.success(responseText);
        } else {
          // 调用失败回调
          this.fail({ status, responseText });
          this.report({ status, responseText });
        }
      }
    };
    // 3. 异常拦截
    this.ajax.onerror = function (e) {
      this.report({ status, responseText });
    };
  }
  setHeaders() {
    Object.keys(this.headers).forEach((key) => {
      this.ajax.setRequestHeader(ket, this.headers[key]);
    });
  }
  report({ status, responseText }) {
    //   ... 上报异常
  }
  get({ url, params }) {
    // url拼接
    this.url = url;
    this.params = params;
    this.paramsJoinUrl(url, params);
    this.ajax.open("GET", url, true);
    // 设置请求头及超时时间
    this.ajax.setRequestHeader("Content-Type", "application/json");    this.commonSet();
    this.ajax.send(null);
  }
  commonSet() {
    // 设置公共请求头
    this.setHeaders();
    // 设置超时时间
    this.ajax.timeout = 2000; // 超时时间,单位是毫秒
  }

  post({ url, data }) {
    this.ajax.open("POST", url, true);
    // 设置请求头及超时时间
    this.ajax.setRequestHeader(
      "Content-Type",
      "application/x-www-form-urlencoded"
    );
    this.commonSet();
    this.ajax.send({ data });
  }
  abort() {
    // 终止该请求。当一个请求被终止,它的  readyState 将被置为 XMLHttpRequest.UNSENT (0),并且请求的 status 置为 0。
    this.ajax.abort();
  }
  paramsJoinUrl() {
    const hasQuestioin = this.url.includes("?") >= 0;
    const arr = [];
    Object.keys(this.params).forEach((key) => {
      arr.push(`${key}=${this.params[key]}`);
    });
    const path = arr.join("&");
    const search = hasQuestioin ? `&${path}` : path;
    this.url += search;
  }
}

2. axios 通过请求响应拦截

const instance = axios.create({
  baseURL: "https://api.example.com",
});
// 添加请求拦截器
axios.interceptors.request.use(
  (config) => {
    return config;
  },
  (err) => {
    return Promise.reject(err);
  }
);

// 添加响应拦截器
axios.interceptors.response.use(
  (res) => {
    const { status, data } = res;
    if (status >= 200 && status < 300) {
      return data;
    } else {
      // 进行上报
      report({ err: res });
    }
    return res;
  },
  (err) => {
    // 进行上报
    report({ err });
    return Promise.reject(err);
  }
);

3. fetch

class Request {
  request(url = "", method, data = {}) {
    return new Promise((resolve, reject) => {
      fetch(url, {
        method, // *GET, POST, PUT, DELETE, etc.
        mode: "cors", // no-cors, *cors, same-origin
        cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
        credentials: "same-origin", // include, *same-origin, omit
        headers: {
          "Content-Type": "application/json",
          // 'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: JSON.stringify(data), // body data type must match "Content-Type" header
      })
        .then((res) => {
          const { status, ok } = res;
          if (ok) {
            resolve(res.json());
          } else {
            reject(res);
            // 进行上报
            report({ err: res });
          }
        })
        .catch((err) => {
          reject(err);

          // 进行上报
          report({ err: res });
        });
    });
  }
}

示例:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta http-equiv="X-UA-Compatible" content="IE=edge" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>我理解的前端 error 监控</title>  </head>  <body>    <!-- 1. img异常 进 onerror 的捕获 -->    <img src="hlll" alt="111" />    <script>      // 2. Promise.reject 进入 unhandledrejection, 结果值是 event.reason      Promise.reject(11111)      //   3. 外部同步异常代码进入 onerror 冒泡      window.addEventListener('error', e => {        console.log('onerror 冒泡', e)      })      // img加载等资源有问题      window.addEventListener(        'error',        e => {          console.log('onerror 捕获', e)        },        true      )      // promise.reject      window.addEventListener(        'unhandledrejection',        e => {          console.log('reject unhandledrejection', e)        },        true      )      try {        setTimeout(() => {          console.log1(222)        }, 10)        console.log(aaa)      } catch (e) {        console.log('try catch', e)      }      console.log(bbb)    </script>  </body></html>

总结:

  1. window.onerror 冒泡方案 用来解决 同步代码异常、setTimeout 异常,window.onerror 捕获方案 用来解决 html 资源加载失败问题

  2. try catch 内部同步代码的异常通过 try...catch...捕获

  3. Promise.reject() 通过 unhandledrejection 捕获

  4. 接口返回状态通过判断状态码上报埋点捕获

文章链接: 

developer.mozilla.org/en-US/docs/…

developer.mozilla.org/en-US/docs/…

developer.mozilla.org/zh-CN/docs/…

www.axios-js.com/zh-cn/docs/

developer.mozilla.org/zh-CN/docs/…