前端异常监控

本文大体围绕下面几点展开讨论:
javascript

  1. JS 处理异常的方式
  2. 上报方式
  3. 异常监控上报常见问题

JS 异常处理

对于 Javascript 而言,咱们面对的仅仅只是异常,异常的出现不会直接致使 JS 引擎崩溃,最多只会使当前执行的任务终止。html

  1. 当前代码块将做为一个任务压入任务队列中,JS 线程会不断地从任务队列中提取任务执行。
  2. 当任务执行过程当中出现异常,且异常没有捕获处理,则会一直沿着调用栈一层层向外抛出,最终终止当前任务的执行。
  3. JS 线程会继续从任务队列中提取下一个任务继续执行。
<script>
  error
  console.log('永远不会执行');
</script>
<script>
  console.log('我继续执行')
</script>
复制代码

在对脚本错误进行上报以前,咱们须要对异常进行处理,程序须要先感知到脚本错误的发生,而后再谈异常上报。前端

脚本错误通常分为两种:语法错误,运行时错误。java

下面就谈谈几种异常监控的处理方式:webpack

try-catch 异常处理ios

try-catch 在咱们的代码中常常见到,经过给代码块进行 try-catch 进行包装后,当代码块发生出错时 catch 将能捕捉到错误的信息,页面也将能够继续执行。git

可是 try-catch 处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误和异步错误就显得无能为力,捕捉不到。github

示例:运行时错误web

try {
  error    // 未定义变量 
} catch(e) {
  console.log('我知道错误了');
  console.log(e);
}
复制代码

然而对于语法错误和异步错误就捕捉不到了。ajax

示例:语法错误

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

通常语法错误在编辑器就会体现出来,常表现的错误信息为:

Uncaught SyntaxError: Invalid or unexpected token xxx

这样。可是这种错误会直接抛出异常,常使程序崩溃,通常在编码时候容易观察获得。

示例:异步错误

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

除非你在 setTimeout 函数中再套上一层 try-catch,不然就没法感知到其错误,但这样代码写起来比较啰嗦。

window.onerror 异常处理

window.onerror 捕获异常能力比 try-catch 稍微强点,不管是异步仍是非异步错误,onerror 都能捕获到运行时错误。

示例:运行时同步错误

/**
 * @param {String}  msg    错误信息
 * @param {String}  url    出错文件
 * @param {Number}  row    行号
 * @param {Number}  col    列号
 * @param {Object}  error  错误详细信息
 */
 window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
error;
复制代码

示例:异步错误

window.onerror = function (msg, url, row, col, error) {
  console.log('我知道异步错误了');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
setTimeout(() => {
  error;
});
复制代码

然而 window.onerror 对于语法错误仍是无能为力,因此咱们在写代码的时候要尽量避免语法错误的,不过通常这样的错误会使得整个页面崩溃,仍是比较容易可以察觉到的。

在实际的使用过程当中,onerror 主要是来捕获预料以外的错误,而 try-catch 则是用来在可预见状况下监控特定的错误,二者结合使用更加高效。

须要注意的是,window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,不然即便是知道异常的发生控制台仍是会显示

Uncaught Error: xxxxx

关于 window.onerror 还有两点须要值得注意

  1. 对于 onerror 这种全局捕获,最好写在全部 JS 脚本的前面,由于你没法保证你写的代码是否出错,若是写在后面,一旦发生错误的话是不会被 onerror 捕获到的。
  2. 另外 onerror 是没法捕获到网络异常的错误。

当咱们遇到

<img src="./404.png">

报 404 网络请求异常的时候,onerror 是没法帮助咱们捕获到异常的。

<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道异步错误了');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<img src="./404.png">
复制代码

因为网络请求异常不会事件冒泡,所以必须在捕获阶段将其捕捉到才行,可是这种方式虽然能够捕捉到网络请求的异常,可是没法判断 HTTP 的状态是 404 仍是其余好比 500 等等,因此还须要配合服务端日志才进行排查分析才能够。

<script>
window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我知道 404 错误了');
  console.log(
    msg, url, row, col, error
  );
  return true;
}, true);
</script>
<img src="./404.png" alt="">
复制代码

这点知识仍是须要知道,要否则用户访问网站,图片 CDN 没法服务,图片加载不出来而开发人员没有察觉就尴尬了。

Promise 错误

经过 Promise 能够帮助咱们解决异步回调地狱的问题,可是一旦 Promise 实例抛出异常而你没有用 catch 去捕获的话,onerror 或 try-catch 也无能为力,没法捕捉到错误。

window.addEventListener('error', (msg, url, row, col, error) => {
  console.log('我感知不到 promise 错误');
  console.log(
    msg, url, row, col, error
  );
}, true);
Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error'
});
复制代码

虽然在写 Promise 实例的时候养成最后写上 catch 函数是个好习惯,可是代码写多了就容易糊涂,忘记写 catch。

因此若是你的应用用到不少的 Promise 实例的话,特别是你在一些基于 promise 的异步库好比 axios 等必定要当心,由于你不知道何时这些异步请求会抛出异常而你并无处理它,因此你最好添加一个 Promise 全局异常捕获事件 unhandledrejection。

window.addEventListener("unhandledrejection", function(e){
  e.preventDefault()
  console.log('我知道 promise 的错误了');
  console.log(e.reason);
  return true;
});
Promise.reject('promise error');
new Promise((resolve, reject) => {
  reject('promise error');
});
new Promise((resolve) => {
  resolve();
}).then(() => {
  throw 'promise error'
});
复制代码

固然,若是你的应用没有作 Promise 全局异常处理的话,那极可能就像某乎首页这样:

异常上报方式

监控拿到报错信息以后,接下来就须要将捕捉到的错误信息发送到信息收集平台上,经常使用的发送形式主要有两种:

  1. 经过 Ajax 发送数据
  2. 动态建立 img 标签的形式

实例 - 动态建立 img 标签进行上报

function report(error) {
  var reportUrl = 'http://xxxx/report';
  new Image().src = reportUrl + 'error=' + error;
}
复制代码

监控上报常见问题

Script error 脚本错误是什么

由于咱们在线上的版本,常常作静态资源 CDN 化,这就会致使咱们常访问的页面跟脚本文件来自不一样的域名,这时候若是没有进行额外的配置,就会容易产生 Script error。

可经过

npm run nocors

查看效果。

Script error 是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error 这样的信息。

这样作的目的是避免数据泄露到不安全的域中,举个简单的例子,

<script src="xxxx.com/login.html"></script>
复制代码

上面咱们并无引入一个 js 文件,而是一个 html,这个 html 是银行的登陆页面,若是你已经登陆了,那 login 页面就会自动跳转到

Welcome xxx…

,若是未登陆则跳转到

Please Login…

,那么报错也会是

Welcome xxx… is not defined,Please Login… is not defined

,经过这些信息能够判断一个用户是否登陆他的账号,给入侵者提供了十分便利的判断渠道,这是至关不安全的。

介绍完背景后,那么咱们应该去解决这个问题?

首先能够想到的方案确定是同源化策略,将 JS 文件内联到 html 或者放到同域下,虽然能简单有效地解决 script error 问题,可是这样没法利用好文件缓存和 CDN 的优点,不推荐使用。正确的方法应该是从根本上解决 script error 的错误。

跨源资源共享机制( CORS )

首先为页面上的 script 标签添加 crossOrigin 属性

// http://localhost:8080/index.html
<script>
  window.onerror = function (msg, url, row, col, error) {
    console.log('我知道错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
<script src="http://localhost:8081/test.js" crossorigin></script>

// http://localhost:8081/test.js
setTimeout(() => {
  console.log(error);
});
复制代码

当你修改完前端代码后,你还须要额外给后端在响应头里加上

Access-Control-Allow-Origin: localhost:8080

,这里我以 Koa 为例。

const Koa = require('koa');
const path = require('path');
const cors = require('koa-cors');
const app = new Koa();

app.use(cors());
app.use(require('koa-static')(path.resolve(__dirname, './public')));

app.listen(8081, () => {
  console.log('koa app listening at 8081')
});
复制代码

读者可经过

npm run cors

详细的跨域知识我就不展开了,有兴趣能够看看我以前写的文章:跨域,你须要知道的全在这里

你觉得这样就完了吗?并无,下面就说一些 Script error 你不常碰见的点:

咱们都知道 JSONP 是用来跨域获取数据的,而且兼容性良好,在一些应用中仍然会使用到,因此你的项目中可能会用这样的代码:

// http://localhost:8080/index.html
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了,但不知道错误信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
function jsonpCallback(data) {
  console.log(data);
}
const url = 'http://localhost:8081/data?callback=jsonpCallback';
const script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
复制代码

由于返回的信息会当作脚本文件来执行,一旦返回的脚本内容出错了,也是没法捕捉到错误的信息。

解决办法也不难,跟以前同样,在添加动态添加脚本的时候加上 crossOrigin,而且在后端配上相应的 CORS 字段便可.

const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
复制代码

读者能够经过

npm run jsonp

查看效果

知道原理以后你可能会以为没什么,不就是给每一个动态生成的脚本添加 crossOrigin 字段嘛,可是在实际工程中,你多是面向不少库来编程,好比使用 jQuery,Seajs 或者 webpack 来异步加载脚本,许多库封装了异步加载脚本的能力,以 jQeury 为例你多是这样来触发异步脚本。

$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})
复制代码

假如这些库中没有提供 crossOrigin 的能力的话(jQuery jsonp 可能有,伪装你不知道),那你只能去修改人家写的源代码了,因此我这里提供一个思路,就是去劫持 document.createElement,从根源上去为每一个动态生成的脚本添加 crossOrigin 字段。

document.createElement = (function() {
  const fn = document.createElement.bind(document);
  return function(type) {
    const result = fn(type);
    if(type === 'script') {
      result.crossOrigin = 'anonymous';
    }
    return result;
  }
})();
window.onerror = function (msg, url, row, col, error) {
  console.log('我知道错误了,也知道错误信息');
  console.log({
    msg,  url,  row, col, error
  })
  return true;
};
$.ajax({
  url: 'http://localhost:8081/data',
  dataType: 'jsonp',
  success: (data) => {
    console.log(data);
  }
})
复制代码

效果也是同样的,读者能够经过

npm run jsonpjq

来查看效果:

这样重写 createElement 理论上没什么问题,可是入侵了本来的代码,不保证必定不会出错,在工程上仍是须要多尝试下看看再使用,可能存在兼容性上问题,若是你以为会出现什么问题的话也欢迎留言讨论下。

关于 Script error 的问题就写到这里,若是你理解了上面的内容,基本上绝大部分的 Script error 都能迎刃而解。

window.onerror 可否捕获 iframe 的错误

当你的页面有使用 iframe 的时候,你须要对你引入的 iframe 作异常监控的处理,不然一旦你引入的 iframe 页面出现了问题,你的主站显示不出来,而你却浑然不知。

首先须要强调,父窗口直接使用 window.onerror 是没法直接捕获,若是你想要捕获 iframe 的异常的话,有分好几种状况。

若是你的 iframe 页面和你的主站是同域名的话,直接给 iframe 添加 onerror 事件便可。

<iframe src="./iframe.html" frameborder="0"></iframe>
<script>
  window.frames[0].onerror = function (msg, url, row, col, error) {
    console.log('我知道 iframe 的错误了,也知道错误信息');
    console.log({
      msg,  url,  row, col, error
    })
    return true;
  };
</script>
复制代码

读者能够经过

npm run iframe

查看效果:

若是你嵌入的 iframe 页面和你的主站不是同个域名的,可是 iframe 内容不属于第三方,是你能够控制的,那么能够经过与 iframe 通讯的方式将异常信息抛给主站接收。与 iframe 通讯的方式有不少,经常使用的如:postMessage,hash 或者 name 字段跨域等等,这里就不展开了,感兴趣的话能够看:跨域,你须要知道的全在这里

若是是非同域且网站不受本身控制的话,除了经过控制台看到详细的错误信息外,没办法捕获,这是出于安全性的考虑,你引入了一个百度首页,人家页面报出的错误凭啥让你去监控呢,这会引出不少安全性的问题。

压缩代码如何定位到脚本异常位置

线上的代码几乎都通过了压缩处理,几十个文件打包成了一个并丑化代码,当咱们收到

a is not defined

的时候,咱们根本不知道这个变量 a 到底是什么含义,此时报错的错误日志显然是无效的。

第一想到的办法是利用 sourcemap 定位到错误代码的具体位置,详细内容能够参考:Sourcemap 定位脚本错误

另外也能够经过在打包的时候,在每一个合并的文件之间添加几行空格,并相应加上一些注释,这样在定位问题的时候很容易能够知道是哪一个文件报的错误,而后再经过一些关键词的搜索,能够快速地定位到问题的所在位置。

收集异常信息量太多,怎么办

若是你的网站访问量很大,假如网页的 PV 有 1kw,那么一个必然的错误发送的信息就有 1kw 条,咱们能够给网站设置一个采集率:

Reporter.send = function(data) {
  // 只采集 30%
  if(Math.random() < 0.3) {
    send(data)      // 上报错误信息
  }
}
复制代码

这个采集率能够经过具体实际的状况来设定,方法多样化,可使用一个随机数,也能够具体根据用户的某些特征来进行断定。

上面差很少是我对前端代码监控的一些理解,提及来容易,可是一旦在工程化运用,不免须要考虑到兼容性等种种问题,读者能够经过本身的具体状况进行调整,前端代码异常监控对于咱们的网站的稳定性起着相当重要的做用。如若文中全部不对的地方,还望指正。

相关文章
相关标签/搜索