前端监控之处理异常的正确姿势
Posted 恪愚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端监控之处理异常的正确姿势相关的知识,希望对你有一定的参考价值。
前端异常
一般来说,根据笔者的目前研究,前端异常大体上可以分为两类:由于对语法的不了解、机制的不清楚或是没有做好降级处理而“主动”造成的错误(多为js异常)和由于资源加载、第三方库、浏览器本身机制造成的“被动异常”。
第二种异常的解决方式很多,通过各种手段也大多可以避免(比如更换源这种常用手段)。我们主要来说一下第一种!
函数的常见处理
我们知道,js中充斥着大量的函数,它们承担封装某个具体功能的作用。但是在看许多代码时,笔者发现了一个问题:我们总喜欢用 return false/true;
来表示该函数内部功能代码的执行错误/正确。
《代码大全2》中说:“使用throw
,而不是return false
”。笔者深以为然。在我司的前端规范中也有类似的话!这很重要。
通过报错,我们可以传递许多字段信息出来,这些信息被捕获到以后可以成为分析用户行为、监控、定位前端异常的利器。我司自研的前端监控工具spider就是这么做的。
常见JS执行错误
SyntaxError
解析时发生语法错误
// 控制台运行
const xx,
window.onerror捕获不到SyntxError,一般SyntaxError在构建阶段,甚至本地开发阶段就会被发现。
TypeError
值不是所期待的类型
// 控制台运行
const person = void 0
person.name
ReferenceError
引用未声明的变量
// 控制台运行
nodefined
RangeError
当一个值不在其所允许的范围或者集合中
(function fn ( ) { fn() })()
网络错误
ResourceError
资源加载错误
new Image().src = '/remote/image/notdeinfed.png'
HttpError
Http请求错误
// 控制台运行
fetch('/remote/notdefined', {})
搜集错误
所有起因来源于错误,那我们如何进行错误捕获。
try/catch
能捕获常规运行时错误,但是对于语法错误和异步错误不行
// 常规运行时错误,可以捕获
try {
console.log(notdefined);
} catch(e) {
console.log('捕获到异常:', e);
}
// 语法错误,不能捕获
try {
const notdefined,
} catch(e) {
console.log('捕获到异常:', e);
}
// 异步错误,不能捕获
try {
setTimeout(() => {
console.log(notdefined);
}, 0)
} catch(e) {
console.log('捕获到异常:',e);
}
try/catch
有它细致处理的优势,但缺点也比较明显。
window.onerror
window.onerror
—— 当 JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件。
/**
* @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});
}
验证下几个错误是否可以捕获:
// 常规运行时错误,可以捕获
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
console.log(notdefined);
// 语法错误,不能捕获
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
const notdefined,
// 异步错误,可以捕获
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
}
setTimeout(() => {
console.log(notdefined);
}, 0)
// 资源错误,不能捕获
<script>
window.onerror = function(message, source, lineno, colno, error) {
console.log('捕获到异常:',{message, source, lineno, colno, error});
return true;
}
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
window.onerror
不能捕获资源错误怎么办?
window.addEventListener
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror
不能监测捕获。
// 图片、script、css加载错误,都能被捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<img src="https://yun.tuia.cn/image/kkk.png">
<script src="https://yun.tuia.cn/foundnull.js"></script>
<link href="https://yun.tuia.cn/foundnull.css" rel="stylesheet"/>
// new Image错误,不能捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
new Image().src = 'https://yun.tuia.cn/image/lll.png'
</script>
// fetch错误,不能捕获
<script>
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
</script>
<script>
fetch('https://tuia.cn/test')
</script>
其中new Image
比较重要(和特别),可以单独自己处理自己的错误。
但通用的fetch怎么办呢,fetch返回Promise,但Promise的错误不能被捕获,怎么办呢?
window.addEventListener
和window.onerror
都不能捕获promise错误
Promise错误的解决
普通Promise错误
try/catch
不能捕获Promise中的错误
// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中
try {
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
})
} catch(err) {
console.error('in try catch', err)
}
// 需要使用catch方法
new Promise((resolve,reject) => {
JSON.parse('')
resolve();
}).catch(err => {
console.log('in catch fn', err)
})
async错误
上面说的普通的 promise 错误可以通过async/await
来解决。但是有一点比较特殊的是:
try/catch
不能捕获 async 包裹的错误!
const getJSON = async () => {
throw new Error('inner error')
}
// 通过try/catch处理
const makeRequest = async () => {
try {
// 捕获不到
JSON.parse(getJSON());
} catch (err) {
console.log('outer', err);
}
};
try {
// try/catch不到
makeRequest()
} catch(err) {
console.error('in try catch', err)
}
try {
// 需要await,才能捕获到
await makeRequest()
} catch(err) {
console.error('in try catch', err)
}
import chunk错误
import 其实返回的也是一个 promise。它是一个特殊的语法 —— 在w3c的不懈努力下,import 支持“不必非要写在最顶部”的写法。对此,我们有如下两种方式捕获错误:
// Promise catch方法
import(/* webpackChunkName: "incentive" */'./index').then(module => {
module.default()
}).catch((err) => {
console.error('in catch fn', err)
})
// await 方法,try catch
try {
const module = await import(/* webpackChunkName: "incentive" */'./index');
module.default()
} catch(err) {
console.error('in try catch', err)
}
小结:全局捕获Promise中的错误
以上三种其实归结为Promise类型错误,可以通过 unhandledrejection API 捕获:
// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
console.log('捕获到异常:', e);
});
为了防止有漏掉的 Promise 异常,可通过 unhandledrejection 用来全局监听Uncaught Promise Error。
Vue中的错误
由于Vue会捕获所有Vue单文件组件或者Vue.extend
继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror
捕获,而是会抛给Vue.config.errorHandler
。
/**
* 全局捕获Vue错误,直接扔出给onerror处理
*/
Vue.config.errorHandler = function (err) {
setTimeout(() => {
throw err
})
}
跨域问题
一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。
如果当前投放页面和云端JS所在不同域名,如果云端JS出现错误,window.onerror
会出现Script Error。通过以下两种方法能给予解决:
后端配置Access-Control-Allow-Origin
、前端script加crossorigin
。
<script src="http://yun.tuia.cn/test.js" crossorigin></script>
const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = 'http://yun.tuia.cn/test.js';
document.body.appendChild(script);
如果不能修改服务端的请求头,可以考虑通过使用 try/catch 绕过,将错误抛出:
<!doctype html>
<html>
<head>
<title>Test page in http://test.com</title>
</head>
<body>
<script src="https://yun.dui88.com/tuia/cdn/remote/testerror.js"></script>
<script>
window.onerror = function (message, url, line, column, error) {
console.log(message, url, line, column, error);
}
try {
foo(); // 调用testerror.js中定义的foo方法
} catch (e) {
throw e;
}
</script>
</body>
</html>
我们捋一下场景,一般调用远端js,有下列三种常见情况。
- 调用远端JS的方法出错
- 远端JS内部的事件出问题
- 要么在setTimeout等回调内出错
上报接口
捕获到了错误,就要开始往服务端发送(可以生成日志或者异常文档,一般监控工具会自动完成)。这其实就是一次请求的过程。
里面有几个需要注意的地方:
为什么不能直接用ajax - GET/POST/HEAD请求接口进行上报?
一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。
为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?
一般来说创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。而且载入js/css资源还会阻塞页面渲染,影响用户体验。
构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点。
使用new Image进行接口上报。最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件。
为什么推荐采用1x1的gif图片进行操作?
首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG。
同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。GIF才是最佳选择。
使用
1*1
的gif
以上是关于前端监控之处理异常的正确姿势的主要内容,如果未能解决你的问题,请参考以下文章