参考文献:
juejin.cn/post/684490…
juejin.cn/post/699330… juejin.cn/post/710084…
背景:为什么要对错误进行监控?
迅速、准确的神不知鬼不觉的修复错误,将错误的影响降到最低。
因为我们项目上线之后如果等到线上问题影响范围和错误持续时间持久的话,那么就会把失误变为事故,影响越大,开发担当的责任就越大。所以必须要在错误发生的第一时间后,就要做错误发现的第二个人。这就是为什么要迅速。
那么我们如果发现了错误,就要立即解决错误。怎么解决?就是要先找到错误发生在哪里?比如:那个页面,对应的哪个组件?组件内的哪一行错误?错误的类型是什么(是数据格式错误还是参数错误)?这些错误的信息巴不得越详细越好,这样就可以帮助我们快速修复。
做到这两点就可以完成快速修复上线,做到用户“神不知鬼不觉”把问题修复了,这样用户就会认为刚刚的错误是幻觉,哈哈。
前端错误监控SDK的设计可以分为两类
1.业务功能的实现:
1.1 错误监控覆盖率(对不同错误类型都可以捕获到)
1.2 错误上报(上报方式、上报时机)
2.SDK的设计
SDK的兼容性
SDK的扩展性
SDK错误的个理性
一、前端的错误分类
基础知识:前端错误类型可以分为6大类
- js编译语法错误:通篇扫描语法错误
- js运行时候错误(又细分为四种)
- promise请求错误
- script标签资源加载错误
- ajax跨域错误
- 框架错误
这六种类型的错误可以大致归为三类:js脚本错误,js第三方框架错误、请求错误(1.promise异步请求、2.ajax跨域、3.避免跨域的-script标签加载)
1. 预编译阶段:语法错误
- 语法错误(SyntaxError)
抛出错误:
Uncaught SyntaxError: Invalid or unexpected token
<script>
const value = 10
@!
console.log(value)
</script>
分析:发生在语法检测阶段。
js是解释型语言,也就是说js的源代码在发往客户端之前不需要进过任何操作(即编译)就可以直接在浏览器中运行(因此可以理解为一边编译一边运行的)。这里简单讲一下,编译型语言和解释型语言的区别: 首先两者都是需要编译和执行的,只不过这两个动作是放在统一阶段执行还是分开两个阶段单独执行。 1.第一个区别就在于代码的编译阶段和运行阶段是否是分开的。解释型是不分开的,但是编译型语言是将编译阶段和解释阶段隔开的(运行之前必须保证编译通过)
2.因为第一个区别,导致解释型语言直接在运行时候编译,因此编译的工作量就挪到了运行时期,这就导致代码运行时候还是需要处理编译的工作,相反编译型语言就不需要在运行时候执行编译,因为之前编译阶段已经把编译的工作处理好了,因此运行的速度来看,编译型语言是高于解释型语言的。 因此,编译型语言牺牲了前期编译的时间,换取了用户肉眼可见的执行时间效率。 JS的编译 js的编译可以分为三个阶段:词法分析、语法分析、代码生成、预编译四个阶段 1.词法分析:将一个个字符识别为词法单元流token 2.语法分析:将上一步的token生成对应的AST树 3.代码生成: 将AST树转化为可以执行的代码 4.预编译:
js引擎在解析scrpit代码时,会先进行“词法分析”, 将js转换成[{}] 格式的tokens流, 为什么要这么做呢? 因为后面要将tokens流转换成抽象语法树, 在生成语法树的过程中,引擎会有语法分析器对语法进行判断, 上述代码语法分析器判断无法生成一颗有效的语法树,抛出语法错误。停止对代码的解析。
2. js运行阶段:js脚本错误(4种常见)
- 变量引用错误(ReferenceError)
抛出的错误
test.html:12 Uncaught ReferenceError: test is not defined
<script>
const value = 10
console.log(test)
console.log(value)
</script>
ReferenceErro译为:“引用错误”,test is not defined:test没有声明。
这个错误是在运行阶段,即运行代码的时候发现的。ast树生成会进行预编译(在js引擎中为变量分配空间和变量提升),预编译之后执行编译进入js运行阶段,当执行到console.log(test)时候发现test在作用域查找,找到window层都没有发现,则报出引用错误(作用域中没有找到就会报出引用错误),js引擎不再向下执行。
- 数组范围错误(RangeError)
<script>
const value = []
value.length = -1
console.log(value)
</script>
这个错误捕获运行阶段, 即运行代码的时候发现。在当预编译完成后,js引擎会进入,代码运行到value.length = -1 时, js引擎发现了value的length被赋值为了-1。此时js引擎抛出 Uncaught RangeError: Invalid array length 。因为-1 值不在数组所允许的范围或者集合中。抛出错误后,js代码终止。
- 数据类型错误(TypeError)
值的类型或参数不是预期类型时发生的错误
抛出错误:Uncaught TypeError: test.go is not a function
<script>
const value = []
const test = {};
test.go();
console.log(value)
</script>
- URIError 对象使用错误
当我们使用encodeURI或者decodeURI报错的时候会出现这个错误
3. 资源加载错误
这类错误就是在代码中引用了不存在的图片,js,css 等静态资源导致的异常
- 资源加载错误
- 自定义请求错误
4. 跨域错误
5. promise请求错误
6. vue错误
总结,前端的错误可以分为三类错误:
1.编译错误,这类错误是发生在编译时期,不会存在于线上报错
2.JS运行错误:这个是发生在代码运行时候的错误:比如变量的引用,数据类型错误不是一个函数当作函数使用等。这些错误可能发生在业务代码中也可能发生在框架中,所以不仅要监听业务代码还要监听框架中抛出的错误,因此JS错误主要分为两类:业务中JS代码错误、前端框架中错误
3.http接口请求错误:接口错误又分为三小类,这里处理包括promise请求接口错误,还包括资源加载错误(例如:img中src加载错误),浏览器跨域请求错误
其中,项目监控中需要监控的是JS错误和http请求错误。
为什么要监控JS错误,或者说我们上线测试的时候JS没有发现错误,为什么有时候会线上发生JS错误呢? 因为前端开发时候取对象属性的时候,有时候会向下取很深的属性,但是如果这个时候后端返回的数据结构改变,那么前端JS代码就会报错。这种报错后端监控是监测不到的,因此我们
二、不同类型的错误监控
1.脚本错误监控:window.error
2.请求错误监控:前端请求有两种方案,使用 ajax 或者 fetch ,所以只需重写两种方法,进行代理,即可实现: window.addEventListener('unhandledrejection').
3.资源错误监控 window.addEventListener('error', e => {})
4.跨域脚本错误捕获window.onerror
5.vue错误Vue.config.errorHandler = function() {}
- try-catch
- window.onerror
- window.addEventListener
- 特殊的:Promise错误
1. try-catch
try-catch有哪些错误是不能捕获的?
根据try只能捕获在线程中执行时候发生的错误,所以对于不在线程中抛出的错误是无法catch住的,那么哪些错误不是在线程中发生的呢?
1.js错误类型中的预编译错误--语法错误(这个错误发生在预编译阶段,不是在执行阶段)
2.异步错误无法捕获-在setTimeout中执行的错误,这个是在定时器线程中执行的错误,所以无法被catch住
3.
只能捕获常规运行时错误,语法错误和异步/promise错误不行
特点: 只能捕获线程进入到try-catch,并且try-catch代码未执行完的时候抛出来。
优点: 处理很细致。
缺点: 捕获常规运行时错误,语法错误和异步错误不行。但是对于 async…await,中异步错误,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 {
new Promise(() => {
throw new Error()
})
} catch (error) {
// 无法捕获错误
}
async function fn () {
try {
await new Promise(() => {
throw new Error()
})
} catch (error) {
// 可以捕获到错误
}
}
问题1:为什么try-catch不能捕获到promise构造函数中的错误?
答1:因为promise源码内部已经包裹了try-catch,被内部的catch住了就不会抛到外面的try-catch了。
2. window.onerror
当页面发生js错误的时候,会触发window.error回调函数。将回调的信息发送给服务端,然后配合source-map可以知道源码中错误的位置。
注意:window.error是唯一可以阻止错误出现在控制台的API(通过设置回调返回值为true) 可以捕获语法错误
可以通过window.onerror捕获。当 JS 运行时错误发生时(包括处理程序中引发的语法错误和异常),window 会触发一个 ErrorEvent 接口的 error 事件,执行对应的事件回调函数。
但是window.onerror捕获不到资源加载错误。如下为window.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">
3. script资源加载错误的捕获
这类错误就是在代码中引用了不存在的图片,js,css 等静态资源导致的异常
当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,这些 error 事件不会向上冒泡到 window,但能被捕获。而window.onerror不能监测捕获。
addEventListener('error', cb, true)(默认冒泡-false,这里需要手动改成true)
在大多数情况下addEventListener('error')和window.onerror的效果差不多。在浏览器中有两种事件机制,捕获和冒泡,这两个方法就分别是通过捕获和冒泡来拿到error的。
但是对于资源的加载错误事件中,canBubble: false,所以理所应当的window.onerror是拿不到资源加载错误的,而addEventListener则可以拿到错误。但是在拿到错误以后需要简单的区分一下是资源加载错误还是其他错误,因为该方法也能够捕获语法错误等一系列其他错误。
方法也很简单,他们之间有一个很明显的区别,其他的普通错误会有一个message字段,资源加载错误没有这个字段,这样只要让这一段代码运行在所有资源之前,那就可以拿到这方面的错误了。
总结:window.onerror是通过冒泡到window对象拿到错误对象,但是script是没有冒泡,所以只有捕获true拿到。
所以综上所诉,error事件可以js的错误(包括同步和异步错误),但是对于语法的错误、请求的错误、script资源加载的错误没有办法捕获到。
window.addEventListener('error', (errorEvent) => {
console.log(errorEvent)
cosnole.log(errorEvent.message)
}, true)
// 图片、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>
4. promise的捕获
handleRejection可以捕获到,promise发生的,并且没有被catch到的错误。 4.1 普通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)
})
4.2 async/await的捕获
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)
}
4.3 import chunk的捕获
> import其实返回的也是一个promise,因此使用如下两种方式捕获错误
// 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类型错误,可以通过unhandledrejection捕获。为了防止有漏掉的 Promise 异常,可通过unhandledrejection用来全局监听Uncaught Promise Error。
// 全局统一处理Promise
window.addEventListener("unhandledrejection", function(e){
console.log('捕获到异常:', e);
});
fetch('https://tuia.cn/test')
所以,综上所诉
1.理解window.error的缺点(资源加载错误可以检测,但是不能捕获,因为不能冒泡,采用evnetlistener捕获阶段获取)
2.try-catch对于promise捕获的特点,为什么不能捕获几种peomise/async/import chunk错误
(1)语法分析错误、js运行时错误、资源加载错误都可以通过window.addEventListener('error',callback)进行监控。
(2)而对于普通Promise/async/import chunk错误可以通过window.addEventListener('unhandledrejection') 进行监控.
4. Vue错误的捕获
由于Vue会捕获所有Vue单文件组件或者Vue.extend继承的代码,所以在Vue里面出现的错误,并不会直接被window.onerror捕获,而是会抛给Vue.config.errorHandler。
* 全局捕获Vue错误,直接扔出给onerror处理
*/
Vue.config.errorHandler = function (err) {
setTimeout(() => {
throw err
})
}
5. 跨域问题的捕获
一般情况,如果出现 Script error 这样的错误,基本上可以确定是出现了跨域问题。对于本域的js抛出的错误,onerror包含了详情的错误信息。对于其他域的js抛出的错误,只会在msg中显示简单的
Script error。
window.onerror = (msg, url, line, column, error) => {
console.log(
`
捕获到错误
${msg},
${url},
${line},
${column},
${JSON.stringify(error)},
`
)
}
如何解决这个问题
- 在script标签上添加,crossorigin属性
- 静态文件服务器开启CORS
我们就可以获得其他域js抛出错误的详细信息了。 注意点:window.onerror和window.addEventListener('error', cb, )
三、错误上报
3.1 上报方式:图片的形式/navigator.sendBeacon
ajax上报缺点
1.跨域问题:上报的服务器地址和上报的地址不是一个地址,所以域名肯定是不一样的,如果使用ajax请求的话,肯定会造成跨域的问题。
2.携带cookie
3.ajax上报的请求会阻塞正常的业务请求,对业务找成影响
4.ajax上报请求丢失问题var ajax = new XMLHTTPRequest(); // 建立ajax实例 ajax.open(‘请求方法’, 参数,)// 使用open方法确定ajax请求方法等细节 ajax.onreadystatechange = () => {}; // http返回的监听 ajax.send(null) // 发出实际请求问题4中,如何确保数据上报请求不会消失
3.1.1 图片方式
通过 new Image 方式创建的元素,只需要赋值 src 属性即可发送请求,无需插入文档中。
需要注意在拼接参数的时候,需要使用 encodeURIComponent 对值进行转移否则将 location.href 这类url作为值时会造成错误。
function parseJsonToString(dataJson) {
if (!dataJson ) { dataJson = {} }
var dataArr = Object.keys(dataJson).map(function(key) { return key + '=' + encodeURIComponent(dataJson[key]) })
return dataArr.join('&')
}
const logGif = (params) => {
const upload = parseJsonToString(params)
const img = new Image(1,1)
img.src = 'https://view-error?' + upload
}
3.1.3 Navigator.sendBeacon
sendbeacon就是为了数据统计而诞生的,首先解决了ajax的跨域问题,然后就是不阻塞业务请求,不会出现请求消失问题。 缺点就是兼容性差一些,部分浏览器不支持可以使用image进行上报。
3.2 上报时机
- 页面加载和重新刷新
- 页面切换路由
- 页面关闭
- 页面所在的tab标签重新可见 一:Ajax上传,Ajax上报就是在上文注释错误捕获的地方发起Ajax请求,来向服务器发送错误信息。
二:利用Image对象发送信息
(new Image()).src="post.error.com?data=xxx"
关键点: 1.错误监控覆盖率:各种错误类型能够捕获到 2.上报:上报方式、上报时机、上报优化 sdk本身的设计:参考juejin.cn/post/710866… 1.可移植性:多平台的支持 2.可扩展性 3.sdk错误的异常隔离
生成错误 uid
首先,什么叫为每个错误生成 uid,这里生成的 uid 有什么用呢?答案其实很简单:
- 一次用户访问(页签未关闭),上报过一次错误后,后续产生
重复错误不再上报 - 多个用户产生的同一个错误,在服务端可以归类,
分析影响用户数、错误数等指标 - 需要注意的是,对于同一个原因产生的同一个错误,生成的 uid 是相同的