前端异常监控
前端异常是我们开发中经常出现的,由于一些条件限制,往往线上的前端异常比较难查找定位,所以如何快速、准确的查找到异常并上报,是快速解决前端问题的关键一步。
一、前端错误类型:
1、ECMAScript exceptions;
2、DOMException;
3、网络静态资源加载错误;
4、跨域引用script导致的script error;
5、页面崩溃。
ECMAScript exceptions
ECMAScript异常就是javascript在执行过程中所发生的错误。每一种错误都有对应的错误类型。当错误发生的时候,就会抛出相应类型的错误对象。主要有以下六种错误类型:
(1) SyntaxError:语法错误
// 1. Syntax Error: 语法错误
// 1.1 变量名不符合规范
var 1 // Uncaught SyntaxError: Unexpected number
var 1a // Uncaught SyntaxError: Invalid or unexpected token
// 1.2 给关键字赋值
function = 5 // Uncaught SyntaxError: Unexpected token =
// 1.3 关键词写错
lett a = 1
(2) Uncaught ReferenceError:引用错误
引用一个不存在的变量时发生的错误。将一个值分配给无法分配的对象,比如对函数的运行结果或者函数赋值。
// 2.1 引用了不存在的变量
a() // Uncaught ReferenceError: a is not defined
console.log(b) // Uncaught ReferenceError: b is not defined
// 2.2 给一个无法被赋值的对象赋值
console.log("abc") = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment
(3)RangeError:范围错误
// 3.1 数组长度为负数
[].length = -5 // Uncaught RangeError: Invalid array length
// 3.2 Number对象的方法参数超出范围
12.89.toFixed(-1) // Uncaught RangeError: toFixed() digits argument must be between 0 and 20 at Number.toFixed
// 说明: toFixed方法的作用是将数字四舍五入为指定小数位数的数字,参数是小数点后的位数,范围为0-20.
(4) TypeError类型错误
变量和参数不是预期类型时报的错,比如调用不存在的方法,使用new操作符后面跟基本类型
// 4.1 调用不存在的方法
123() // Uncaught TypeError: 123 is not a function
var o = {}
o.run() // Uncaught TypeError: o.run is not a function
// 4.2 new关键字后接基本类型
var p = new 456 // Uncaught TypeError: 456 is not a constructor
(5) URIError,URL错误
decodeURI("%") // Uncaught URIError: URI malformed at decodeURI
URI相关参数不正确时抛出的错误,主要涉及encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()六个函数。
(6)EvalError eval()函数执行错误
eval("2+3") // 返回 5
var myeval = eval; // 可能会抛出 EvalError 异常
myeval("2+3"); // 可能会抛出 EvalError 异常
需要注意的是:ES5以上的JavaScript中已经不再抛出该错误,但依然可以通过new关键字来自定义该类型的错误提示。
DOMException
DOMException就是我们在调用web API方法或者属性的时候所发生的错误,或者简单地说是在执行DOM操作的时候所抛出的错误。
比如在错误地调用了DOM接口:
浏览器报错:Uncaught DOMException: Failed to execute 'createAttribute' on 'Document': The localName provided ('123') contains an invalid character.
网络静态资源加载错误
常见的网络静态资源包括有,html文件,css文件,javascript文件,图片,音频,视频,iframe等等。所有的网络静态资源都有可能遇到加载错误这个问题。加载错误的原因也不一而定,有可能是url写错了,有可能是服务器根本就没有这个资源,有可能是服务器出现内部错误,也有可能是网络忙,请求超时而导致资源加载失败。不管什么原因,凡是客户端没有加载成功的,一律视为出现了加载错误。
script error
当跨域引用了另外一个域下javascript资源,并且这个javascript执出错的情况下,浏览器就会抛出这个错误
假如,我们在your.com域名下有这么一个js文件:
const a = {};
console.log(a.b.c);
我们在mine.com 去引用这个文件
那么这种场景下,浏览器就会抛出一个script error类型的错误。script error类型的错误基本上是在告诉你,你跨域引用的脚本执行出错了,但是你没有知道具体错误信息的权利。
页面崩溃
当你访问一个不靠谱的web应用的时候,它可能会做出一个疯狂和不可预测的操作,这个时候,就会导致整个浏览器的崩溃..
二、错误处理
错误处理的几个方法:
- try...catch
- window.onerror
- window.addEventListener('error',()=>{})
- Promise Catch与window.addEventListener("unhandledrejection",()=> {})
- iframe与iframe.onload
- 其他
try...catch
try-catch 只能捕获到同步的运行时错误,对语法和异步错误却无能为力,捕获不到。
1、同步运行时错误:
try {
let name = 'jartto';
console.log(nam);
} catch(e) {
console.log('捕获到异常:',e);
}
捕获到异常:ReferenceError: nam is not defined
at :3:15
2、不能捕获到具体的语法错误,只有一个语法错误提示。我们修改一下代码,删掉一个单引号:
try {
let name = 'jartto;
console.log(nam);
} catch(e) {
console.log('捕获到异常:',e);
}
Uncaught SyntaxError: Invalid or unexpected token
不过语法错误在我们开发阶段就可以看到,应该不会顺利上到线上环境。
3、异步错误
try {
setTimeout(() => {
undefined.map(v => v);
}, 1000)
} catch(e) {
console.log('捕获到异常:',e);
}
Uncaught TypeError: Cannot read property 'map' of undefined
at setTimeout (:3:11)
window.onerror
当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。
/**
* @param {String} message 错误信息
* @param {String} source 出错文件
* @param {Number} lineno 行号
* @param {Number} colno 列号
* @param {Object} error Error对象(对象)
*/
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
1、同步代码运行错误
window.onerror = function(message, source, lineno, colno, error) {
// message:错误信息(字符串)。
// source:发生错误的脚本URL(字符串)
// lineno:发生错误的行号(数字)
// colno:发生错误的列号(数字)
// error:Error对象(对象)
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
Jartto;
可以看到,我们捕获到了异常:
2、再试试语法错误
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
let name = 'Jartto
Uncaught SyntaxError: Invalid or unexpected token
没有捕获语法错误
3、异步代码错误
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
Jartto;
});
捕获到异常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined
at setTimeout (http://127.0.0.1:8001/:36:5)}
setTimeout、setInterval的错误是可以捕获的,但是其他的比如Promise、async里面的错误不能捕获:
window.onerror = function (message, source, lineno, colno, error) {
console.log('window.onerror捕获的异常:', message)
return true
}
new Promise((resolve, reject) => {
console.log(a)
resolve(1)
})
async function test() {
console.log(a)
}
4、接着,我们试试网络请求异常的情况:
我们发现,不论是静态资源异常,或者接口异常,错误都无法捕获到
window.onerror 函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示 Uncaught Error: xxxxx
需要注意:
- onerror 最好写在所有 JS 脚本的前面,否则有可能捕获不到错误;
- onerror 无法捕获语法错误;
在实际的使用过程中,onerror 主要是来捕获预料之外的错误,而 try-catch 则是用来在可预见情况下监控特定的错误,两者结合使用更加高效。
window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,不过(至少在 Firefox 中)能被单一的window.addEventListener 捕获。
window.addEventListener('error', (error) => {
console.log('addEventListener捕获到异常:', error);
}, true)
这种方法不能判断资源加载异常是404还是500等,需要配合后台日志查询
需要注意:
不同浏览器下返回的 error 对象可能不同,需要注意兼容处理。
需要注意避免 addEventListener 重复监听。
Promise Catch
在 promise 中使用 catch 可以非常方便的捕获到异步 error。
没有写 catch 的 Promise 中抛出的错误无法被 onerror 或 try-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。
解决方案:为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听Uncaught Promise Error。
window.addEventListener("unhandledrejection", function(e){
e.preventDefault() // 用于去除控制台的报错
console.log('捕获到异常:', e);
return true;
});
new Promise((resolve, reject) => {
console.log(cxc)
})
iframe与iframe.onload
用window.onerror处理
Page Crash
if(sessionStorage.getItem('good_exit') &&
sessionStorage.getItem('good_exit') !== 'true') {
/*
insert crash logging code here
*/
alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}
window.addEventListener('load', function () {
sessionStorage.setItem('good_exit', 'pending');
setInterval(function () {
sessionStorage.setItem('time_before_crash', new Date().toString());
}, 1000);
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('good_exit', 'true');
});
处理页面崩溃的原理是,崩溃的页面关闭时是无法触发beforeunload的。当第一次进去页面是崩溃的时候,good_exit是pending状态,刷新后,因为无法监听到beforeunload,再次进去的时候还是pending,这时候就可以断定崩溃了。
基于 Service Worker 的崩溃统计方案:
- Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
- Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;
- 网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。
方案:
- p1:网页加载后,通过 postMessage API 每 5s 给 sw 发送一个心跳,表示自己的在线,sw 将在线的网页登记下来,更新登记时间;
- p2:网页在 beforeunload 时,通过 postMessage API 告知自己已经正常关闭,sw 将登记的网页清除;
- p3:如果网页在运行的过程中 crash 了,sw 中的 running 状态将不会被清除,更新时间停留在奔溃前的最后一次心跳;
- p4:Service Worker 每 10s - 查看一遍登记中的网页,发现登记时间已经超出了一定时间(比如 15s)即可判定该网页 crash 了。
Script Error
一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。
特别注意,服务器端需要设置:Access-Control-Allow-Origin
Vue中错误的处理
errorHandler
指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例
// main.js
Vue.config.errorHandler = function (err, vm, info) {
#处理错误信息, 进行错误上报
#err错误对象
#vm Vue实例
#`info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
#只在 2.2.0+ 可用
}
从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩溃。
从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了。
从 2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。
errorCaptured
errorCaptured
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播
举个例子:
Vue.component('cat', {
template:`
Cat:
`,
props:{
name:{
required:true,
type:String
}
},
errorCaptured(err,vm,info) {
console.log(`cat EC: ${err.toString()}\ninfo: ${info}`);
return false;
}
});
Vue.component('kitten', {
template:'Kitten: {{ dontexist() }}
',
props:{
name:{
required:true,
type:String
}
}
});
其中kitten是有bug的
运行结果:
cat EC: TypeError: dontexist is not a function
info: render
三、错误上报
无论是埋点上报还是错误上报,常用方案就是使用Image对象来发送请求。这么做,有以下几点好处:
1、浏览器兼容性好。所有浏览器都支持Image对象。
2、可以避免same-origin-policy的限制。实际开发中,通常都是一台服务器负责接收所有的服务器的错误上报。而这种情况下,就会存在跨域问题,单纯的XMLHttpRequest是解决不了这个问题。
3、在上报过程上,本身出现错误的概率比较低。如果你自己封装了一个ajax库或者使用了外部ajax库,如果这些库本身就有问题的话,你还指望它去上报的话,可想而知,结果是无法上报成功的
下面,我们简单实现一个用于上报错误的方法:
function postError(type, msg) {
const img = new Image();
img.src = `log.php?type=${encodeURICoponent(type)}&msg=${encodeURICoponent(msg)}`
}
设置采集率:
postError.send = function(data) {
// 只采集 30%
if(Math.random() < 0.3) {
send(data) // 上报错误信息
}
}