零散专题37 前端代码异常监控

前端代码异常监控学习笔记,原文地址:前端代码异常监控实战@掘金

异常处理

JavaScript中的异常并不会导致JavaScript引擎崩溃,最多只会让当前执行的任务停止。

1
2
3
4
5
6
7
8
<script>
console.log(error);
console.log('不会执行');
</script>

<script>
console.log('继续执行');
</script>

在对错误及哦啊本进行上报之前,需要对异常进行处理,程序首先要感知到脚本错误的发生,然后再进行异常上报

我们需要处理的错误根据导致的原因分类有以下几种:

  1. 语法错误
  2. 运行时错误
  3. 网络请求错误
  4. iframe中的错误

根据抛出错误的代码所属的任务队列可以分为:

  1. 同步错误
  2. 异步错误(非Promise)
  3. Promise错误

try...catch异常处理

通过使用try...catch结构包裹代码,当try代码块发生错误时,catch能捕获到错误的信息,页面可以继续执行。

1
2
3
4
5
try {
console.log(error); // 运行时错误
} catch(e) {
console.log('捕捉到了');
}

但是try...catch处理异常的能力有限,只能捕获到运行时非异步错误,对于语法错误和异步错误都捕捉不到

1
2
3
4
5
try {
console.log('error'; // 语法错误,少了半个括号
} catch(e) {
console.log('捕捉到了', e);
}

异步错误:

1
2
3
4
5
6
7
try {
setTimeout(() => {
console.log(error); // 异步错误
})
} catch(e) {
console.log('捕捉到了', e);
}

error事件

可以在window.onerror事件中捕获异常,它的捕获异常能力优于try...catch,主要原因在于:

  1. window.onerror是全局捕获,而try...catch针对的是具体的代码块
  2. window.onerror可以捕获同步错误以及异步错误
1
2
3
4
5
window.onerror = function (msg, url, row, col, error) {
console.log('捕获到了错误');
console.log({msg, url, row, col ,error});
return true;
};

同步错误:

1
console.log(error); // 同步错误

异步错误:

1
2
3
setTimeout(() => {
console.log(error); // 异步错误
})

但是window.onerror也无法捕获语法错误。所以在实际使用时,一般使用onerror来捕获预料之外的错误,try...catch用来在可预见情况下监控特定的情况,二者需要结合使用。

有几点需要注意

(1)window.onerror只有在返回true的时候,异常才不会向上抛出,否则即使捕获到了异常,控制台还是会显示Uncaught Error

1
2
3
4
5
6
window.onerror = function (msg, url, row, col, error) {
console.log('捕获到了错误');
console.log({msg, url, row, col ,error});
// return true;
};
console.log(error);

(2)包含window.onerror代码的脚本最好写在所有脚本的最前面,如果放在某些脚本的后面,那么发生错误就无法捕获

1
2
3
4
5
6
7
8
9
10
11
<script>
console.log(error);
</script>

<script>
window.onerror = function (msg, url, row, col, error) {
console.log('捕获到了错误');
console.log({msg, url, row, col ,error});
// return true;
};
</script>

(3)window.onerror无法捕获网络异常的错误

例如当图片加载失败发返回404的异常时,onerror是捕获不到的:

1
2
3
4
5
6
7
8
9
<script>
window.onerror = function (msg, url, row, col, error) {
console.log('捕获到了错误');
console.log({msg, url, row, col ,error});
return true;
};
</script>

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

这是因为网络请求的错误不会事件冒泡,所以必须在捕获阶段去捕获异常:

1
2
3
4
5
6
7
8
9
<script>
window.addEventListener('error', e => {
console.log('捕获到了错误');
console.log(e);
return true;
}, true)
</script>

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

虽然可以捕获到网络请求的异常,但是无法具体判断HTTP的状态和返回码,需要配合服务端日志以及对网络请求进行拦截进行排查分析才可以。

unhandledrejection

try..catchwindow.onerror都无法捕获在Promise中抛出的错误:

1
2
3
4
5
6
7
8
9
10
window.onerror = function (e) {
console.log('捕获到Promise的错误');
return true;
};

try {
Promise.resolve().then(v => console.log(error))
} catch(e) {
console.log('捕获到Promise的错误');
}

对于Promise来说,虽然可以再每一个实例后面添加catch来捕获特定的实例中抛出的错误,但是通过unhandledrejection来全局捕获异常也是非常必要的:

1
2
3
4
5
6
7
window.addEventListener('unhandledrejection', e => {
console.log('捕获到Promise的错误');
console.log(e);
e.preventDefault();
});

Promise.resolve().then(v => console.log(error))

当然如果异常在Promise的实例中进行了捕获,那么在unhandledrejection中就捕获不到了:

1
2
3
4
5
6
7
window.addEventListener('unhandledrejection', e => {
console.log('全部捕获到了');
console.log(e);
e.preventDefault();
});

Promise.resolve().then(v => console.log(error)).catch(e => console.log('实例中捕获到了'))

iframe中的错误

如果页面中使用了<iframe>,需要对引入的<iframe>进行异常监控,否则如果引入的页面出现问题,内容显示不出来,但是我们却一无所知。

父窗口直接使用window.error是无法直接捕获<iframe>中的错误的,需要分下面几种情况进行处理:

(1)<iframe>页面与当前页面同域名的话,可以直接给<iframe>添加onerror事件:

1
2
3
4
5
6
7
window.frames[0].onerror = function (msg, url, row, col, error ) {
console.log('捕获到了iframe中的错误');
console.log({
msg, url, row, col, error
});
return true;
}

(2)如果嵌入的<iframe>中的页面与当前页面不是同一个域名,那么上面的方法就行不通了。如果我们拥有<iframe>的控制权的话,可以通过与<iframe>通信的方式将异常信息发送给主页面,通信的方式很多,比如postMessage事件等

iframe中发送消息,注意需要使用window.top进行发送,不能直接使用window

1
2
3
4
5
6
7
8
9
10
// iframe 中

window.onerror = function(e) {
if (window.top !== window) {
top.postMessage(e, 'http://localhost:63342/demos/demo36-瀑布流/瀑布流-Flex.html')
return true;
}
return true;
}
console.log(error); // 错误

在主页面中需要监听message事件:

1
2
3
4
window.addEventListener('message', (e) => {
console.log('捕获到了iframe中的错误');
console.log(e.data);
})

(3)<iframe>的域名非同源,并且自己没有控制权,那么除了看控制台之外,没有办法捕获。

异常上报

监控拿到报错信息后,需要将捕获到的错误信息上报到信息收集平台上,常用的上报方法有两种:

  1. 通过Ajax发送数据
  2. 通过动态创建<img>标签

通过Ajax发送数据就是平常的调用接口的形式进行上报即可,而动态创建<img>标签主要是根据信息收集平台的要求,将<img>src属性拼接指定的错误信息即可:

1
2
3
4
function report(error) {
const reportUrl = 'http://xxxx/report';
new Image().src = reportUrl + '?error=' + error;
}

Script Error

在线上的版本,静态资源一般会放在CDN,会导致长访问的页面与脚本来自不同的域名,,如果没有额外的配置,就会产生Script Error

这是因为浏览器同源策略导致的,浏览器出于安全性的考虑,当页面引用非同域名的外部脚本文件发生错误时,当前页面是没有权利知道报错信息的,取而代之的是输出Script Error

解决方法就是为页面上的<script>标签添加crossOrigin属性,可以取值anonymoususe-credentials,如果没有属性值或者非法属性值,会被浏览器默认做anonymous,它的作用有三个:

  1. 让浏览器启动CORS访问检查,检查HTTP响应头的Access-Control-Allow-Origin
  2. 对于传统<script>需要跨域获取的JS资源,暴露其报错的详细信息
  3. 对于module script,控制用于跨域请求的凭据模式

同时服务端需要在返回的响应头中Access-Control-Allow-Origin为对应的域名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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);
});

这样就可以获得外域脚本的详细的报错信息了,对于JSONP来跨域的时候遇到的错误可以据此解决:

1
2
3
4
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);

如果使用的第三方库加载异步脚本时,如果没有提供crossOrigin的能力时,可以劫持document.createElement,在动态生成的脚本上添加crossOrigin字段,但是这种方式
有一定风险,需要谨慎使用

1
2
3
4
5
6
7
8
9
10
document.createElement = (function() {
const fn = document.createElement.bind(document);
return function(type) {
const result = fn(type);
if(type === 'script') {
result.crossOrigin = 'anonymous';
}
return result;
}
})();

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

线上的代码都经过了压缩、合并处理,当收到一条报错的信息的时候,可能根本没有办法直接获知变量代表什么、位置在哪里,此时的错误日志是无效的。

解决方法就是利用sourceamp来定位到错误代码的具体位置,详细内容参考《脚本错误量极致优化-让脚本错误一目了然》,最理想的方案可以尝试Sentry开源方案

收集信息太多

可以给网站设置采集率:

1
2
3
4
5
6
Reporter.send = function (data) {
// 只采集 30%
if (Math.random() < 0.3) {
send(data) // 上报错误信息
}
}

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

总结

异常捕获有三种常用的方式try...catchwindow.onerrorunhandledrejection,它们都没有办法捕获语法错误,所以在写代码的时候尽量通过IDE的提示和ESLint的检查避免语法错误

  • try...catch可以捕获同步错误,无法捕获网络错误、异步错误和Promise的错误
  • window.onerror可以捕获同步错误、异步错误,需要在事件捕获阶段捕获网络错误(但是信息不详细),无法捕获Promise的错误
  • unhandledrejection专门用来捕获没有在Promise实例的catch中捕获的Promise中抛出的错误

使用的时候一般:

  • 使用window.onerror来捕获全局的同步错误、异步错误
  • unhandledrejection来捕获全局的Promise错误
  • try...catch来在预见情况下捕获特定的错误

<iframe>中的错误如果是同源页面,可以直接为其添加onerror事件,如果是非同源但是有控制权,可以通过postMessage传递错误,如果是非同源也无控制权,那么无能为力。

参考