前端错误监控指南

前言

做为一个前端,在开发过程即使十分当心,自测充分,在不一样用户复杂的操做下也不免会出现程序员意想不到的问题,给公司或我的带来巨大的损失。 这时一款可以及时上报错误和可以帮助程序员很好的解决错误的前端错误监控系统就必不可少了。 接下来咱们就聊聊常见的错误发生与处理。javascript

本文主要围绕如下几点讨论:html

  1. 常见JS错误类型
  2. 常见JS处理错误方式
  3. 上报的方式,和上报内容的几点思考

问题:前端

  1. JS、CSS、img等资源加载失败(CDN或图床挂了,无心删了、文件名变了)怎么实时获知?而不是用户告诉你?
  2. 如何上报有用的错误信息可以让程序员快速定位错误并修复?而不是上报一些迷惑信息?
  3. 在当今无不用压缩丑化代码的工程化中,怎么利用好 SourceMap 文件,处理错误信息?
  4. 如何出了问题,不用在让用户帮助你复现?要机型?要操做步骤?
  5. 如何更好统计问题的分布(机型设备、浏览器、地理位置、带宽等),自主根据数据来取舍兼容倾向性?
  6. ...

常见错误

  1. 脚本错误
    • 语法错误
    • 运行时错误
      • 同步错误
      • 异步错误
      • Promise 错误
  2. 网络错误
    • 资源加载错误
    • 自定义请求错误

能够阅读监控类库源码 errorWatch 来加深理解,也能够直接用于项目。java

语法错误

例如,英文字符写成中文字符。通常容易在开发时被发现。git

syntaxError

语法错误没法被try catch 处理程序员

try {
  const error = 'error'// 圆角分号
} catch(e) {
  console.log('我感知不到错误');
}
复制代码

同步错误

JS引擎在执行脚本时,把任务分块压入事件栈,轮询取出执行,每一个事件任务都有本身的上下文环境, 在当前上下文环境同步执行的代码发生错误都能被try catch 捕获,保证后续的同步代码被执行。github

try {
  error
} catch(e) {
  console.log(e);
}
复制代码

异步错误

常见的 setTimeout 等方法会建立新的事件任务插入事件栈中,待后续执行。 因此try catch 没法捕获其余上下文的代码错误。web

try {
  setTimeout(() => {
    error        // 异步错误
  })
} catch(e) {
  console.log('我感知不到错误');
}
复制代码

为了便于分析发生的错误,通常利用 window.onerror 事件来监听错误的发生。 它比try catch的捕获错误信息的能力要强大。ajax

/** * @param {String} msg 错误描述 * @param {String} url 报错文件 * @param {Number} row 行号 * @param {Number} col 列号 * @param {Object} error 错误Error对象 */
 window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了');
  // return true; // 返回 true 的时候,异常不会向上抛出,控制台不会输出错误
};
复制代码

windowOnerror

  • window.onerror 注意事项
  1. window.onerror 能够捕获常见语法、同步、异步错误等错误;
  2. window.onerror 没法捕获 Promise 错误、网络错误;
  3. window.onerror 应该在全部JS脚本以前被执行,以避免遗漏;
  4. window.onerror 容易被覆盖,在处理回调时应该考虑,被人也在使用该事件监听。

网络错误

因为网络请求异常不会冒泡,应此须要在事件捕获阶段才能获取到。 咱们能够利用 window.addEventListener。好比代码、图片等重要 CDN 资源挂了,能及时得到反馈是极为重要的。chrome

window.addEventListener('error', (error) => {
  console.log('404 错误');
  console.log(error);
  // return true; // 中断事件传播
}, true);
复制代码

addEventListener

对于这类资源加载错误,在事件对象中能得到足够的信息,配合短信、钉钉等第一时间通知开发者。

window.addEventListener('error', (e) => {
  if (e.target !== window) { // 避免重复上报
    console.log({
    	url: window.location.href, // 引用资源地址
    	srcUrl: e.target.src, // 资源加载出错地址
    })
  }
}, true);
复制代码
  • window.onerrorwindow.addEventListener

window.addEventListener 的好处,不怕回调被覆盖,能够监听多个回调函数,但记得销毁避免内存泄漏与错误。 但没法获取 window.onerror 那么丰富的信息。通常只用window.addEventListener 来监控资源加载错误。

  • 对于网络请求自定义错误,最好是手动上报。

Promise 错误

若是你在使用 promise 时未 catch 的话,那么 onerror 也无能为力了。

Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error';
});
复制代码

一样你能够利用 window.onunhandledrejectionwindow.addEventListener("unhandledrejection")来监控错误。 接收一个PromiseError对象,能够解析错误对象中的 reason 属性,有点相似 stack

具体兼容处理在 TraceKit.js 能够看到。

上报方式

  1. img 上报
  2. ajax 上报
function report(errInfo) {
  new Image().src = 'http://your-api-website?data=' + errInfo;
}
复制代码

ajax 应使用的类库而已,大同小异。

  • 注意:img 请求有长度限制,数据太大最好仍是用 ajax.post

补充

Script error

引用不一样域名的脚本,若是没有特殊处理,报错误了,通常浏览器处于安全考虑,不显示具体错误而是 Script error. 例如他人别有用心引用你的线上非开源业务代码,你的脚本报错信息固然不想让他知道了。

若是解决自有脚本的跨域报错问题?

  • 全部资源切换到统一域名,可是这样就失去了 CDN 的优点。
  • 在脚本文件的 HTTP response header 中设置 CORS
  1. Access-Control-Allow-Origin: You-allow-origin
  2. script 标签中添加 crossorigin 属性,例如 <script src="http://www.xxx.com/index.js" crossorigin></script>

响应头和crossorigin取值问题

  1. crossorigin="anonymous"(默认),CORS 不等于 You-allow-origin,不能带 cookie
  2. crossorigin="use-credentials"Access-Control-Allow-Credentials: true ,CORS 不能设置为 *,能带 cookie。 若是 CORS 不等于 You-allow-origin,浏览器不加载 js。

当你对自由能掌握的资源作好了 cors 时,Script error 基本能够过滤掉,不上报。

讲了这么多,还有一个很是重要的主题,如何分析我能捕获的错误信息?

JavaScript 错误剖析

一个 JavaScript 错误一般由一下错误组成

  • 错误信息(error message)
  • 追溯栈(stack trace)

error

consoleError

开发者能够经过不一样方式来抛出一个JavaScript 错误:

  • throw new Error('Problem description.')
  • throw Error('Problem description.') <-- equivalent to the first one
  • throw 'Problem description.' <-- bad
  • throw null <-- even worse

推荐使用第二种,第三四种浏览器没法就以上两种方式生成追溯栈。

若是能解析每行追溯栈中的错误信息,行列在配合 SourceMap 不就能定位到每行具体源代码了吗。 问题在于不一样浏览器在以上信息给出中,并无一个通用标准的格式。难点就在于解决兼容性问题。

例如 window.onerror 第五个参数 error 对象是2013年加入到 WHATWG 规范中的。 早期Safari 和 IE10尚未,Firefox是从14版本加入Error对象的,chrome 也是 2013 年才新加的。

推荐作法

  1. window.onerror是捕获JS 错误最好的方法,当有一个合法的Error对象和追溯栈时才上报。 也能够避免一些没法干扰的错误,例如插件错误和跨域等一些信息不全的错误。

  2. try catch 加强,抛出的错误信息较全,能够弥补 window.onerror 的不足。但就像先前说过的, try catch 没法捕获异步错误和promise错误,也不利用 V8 引擎性能优化。

例如腾讯的 BadJS,对如下推荐进行了try catch包裹

  • setTimeout 和 setInterval
  • 事件绑定
  • ajax callback
  • define 和 require
  • 业务主入口

具体是否须要作到如此细粒度的包裹,仍是视状况而定。

SourceMap

例若有如下错误追溯栈(stack trace)

ReferenceError: thisIsAbug is not defined
    at Object.makeError (http://localhost:7001/public/js/traceKit.min.js:1:9435)
    at http://localhost:7001/public/demo.html:28:12
复制代码

可以解析成一下格式

[
	{
	  "args" : [],
	  "url" : "http://localhost:7001/public/js/traceKit.min.js",
	  "func" : "Object.makeError",
	  "line" : 1,
	  "column" : 9435,
	  "context" : null
	}, 
	{
	  "args" : [],
	  "url" : "http://localhost:7001/public/demo.html",
	  "func" : "?",
	  "line" : 28,
	  "column" : 12,
	  "context" : null
	}
]
复制代码

在有了行列和对应的 SourceMap 文件就能解析获取源代码信息了。

sourceMapDel

解析结果

sourceMapDel

处理代码以下:

import { SourceMapConsumer } from 'source-map';

// 必须初始化
SourceMapConsumer.initialize({
  'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm',
});

/** * 根据sourceMap文件解析源代码 * @param {String} rawSourceMap sourceMap文件 * @param {Number} line 压缩代码报错行 * @param {Number} column 压缩代码报错列 * @param {Number} offset 设置返回临近行数 * @returns {Promise<{context: string, originLine: number | null, source: string | null}>} * context:源码错误行和上下附近的 offset 行,originLine:源码报错行,source:源码文件名 */
export const sourceMapDeal = async (rawSourceMap, line, column, offset) => {
  // 经过sourceMap库转换为sourceMapConsumer对象
  const consumer = await new SourceMapConsumer(rawSourceMap);
  // 传入要查找的行列数,查找到压缩前的源文件及行列数
  const sm = consumer.originalPositionFor({
    line, // 压缩后的行数
    column, // 压缩后的列数
  });
  // 压缩前的全部源文件列表
  const { sources } = consumer;
  // 根据查到的source,到源文件列表中查找索引位置
  const smIndex = sources.indexOf(sm.source);
  // 到源码列表中查到源代码
  const smContent = consumer.sourcesContent[smIndex];
  // 将源代码串按"行结束标记"拆分为数组形式
  const rawLines = smContent.split(/\r?\n/g);
  let begin = sm.line - offset;
  const end = sm.line + offset + 1;
  begin = begin < 0 ? 0 : begin;
  const context = rawLines.slice(begin, end).join('\n');
  // 记得销毁
  consumer.destroy();
  return {
    context,
    originLine: sm.line + 1, // line 是从 0 开始数,因此 +1
    source: sm.source,
  }
};
复制代码

你们根据 SourceMap 文件的格式,就能很好的理解这段代码了。

参考网站

  1. mozilla/source-map
  2. 前端代码异常监控实战
  3. 前端异常监控 - BadJS
  4. 脚本错误量极致优化-让脚本错误一目了然
相关文章
相关标签/搜索