前端异常捕获且日志上报处理
JavaScript 网页异常捕获
一、异常大概分类
一般我们想要捕获的异常大概分类:
语法错误
以上情况都可以用onerror捕获语法错误
setTimeout(() => { eval('function()') }, 1000); // Uncaught SyntaxError: Function statements require a function name复制代码
onerror 事件代码块与
语法错误代码块
不在一起,例如trycatche或者同在一个代码块,但是
语法错误代码块
异步执行引用错误,类型错误,uri 错误,范围错误等等
非try catch包裹情况下,可以使用 onerror 捕获同步错误、异步错误
console.log(a) // Uncaught ReferenceError: a is not defined Array.test() // 调用了 Array 上不存在的 test,值为 undefined,作为函数执行,则会抛出类型错误 // Uncaught TypeError: Array.test is not a function new Array(12221312312) // Uncaught RangeError: Invalid array length decodeURI('%') // Uncaught URIError: URI malformed复制代码
try{} catch{}
若 try 代码块报错,只能在 catch 中捕获。但是 try 代码块中若有异步错误代码,catch 无法捕获,会被 onerror 捕获
try { setTimeout(() => { console.log('a', a) // 可以被 onerror 捕获 }, 1000) } catch(e) { console.log('e', e) } // Uncaught ReferenceError: a is not defined复制代码
Promise 抛出错误
let p = new Promise((resolve, reject) => { reject(1) } ) // 这里没有做 catch 处理 // Uncaught (in promise) 1复制代码
;(async function xx() { try { throw 1 } catch(e) { console.log('a', a) // 这里出错可以使用 unhandledrejection 来捕获 } })() // Uncaught ReferenceError: a is not defined复制代码
上面两种情况可以监听 unhandledrejection 捕获错误
window.addEventListener('unhandledrejection', function(e) { // e.preventDefault(); // 阻止异常向上抛出 console.log('捕获到异常 unhandledrejection :', e) })复制代码
在 catch 中报错没有捕获
没有设置 catch 捕获
静态资源加载失败
// html // <img src="" alt="" id="imgID"> // js let imgID = document.getElementById('imgID') imgID.onerror = function(e) { console.log('img load error :>> ', e); } // 注意:onerror 需要定义之后,再设置图片路径,才能捕获到加载失败 imgID.src = 'http://xxx.png'复制代码
chrome、FF 中可以通过冒泡方式监听 error 事件捕获资源加载失败
// 注意:此处会与上面的 onerror 事件一起触发,将导致日志重复上报 // 可以只使用 addEventListener 捕获模式统一监听, 就不需要注册 window.onerror 了 window.addEventListener( 'error', error => { console.log('addEventListener 捕获到异常:', error) }, true )复制代码
静态资源网络请求失败事件不会冒泡,需要在捕获阶段捕获
在资源上添加 onerror 事件
网页崩溃
// 初次进来,将埋入一个标志,值为 pending ,正常退出后,会设置为 true // 若网页崩溃,第二次回来页面后,读取当前标志,如果值存在且为 true 则表示正常退出 // 如果不是 true ,则表示上次可能是崩溃了,需要上报之前定时更新的时间值 if(localStorage.getItem('good_exit') && localStorage.getItem('good_exit') !== 'true') { localStorage.getItem('time_before_crash') // 日志上报,将崩溃之前的时间一起上报 } window.addEventListener('load', function () { localStorage.setItem('good_exit', 'pending'); // 定时更新崩溃之前的网页时间 setInterval(function () { localStorage.setItem('time_before_crash', new Date().toString()); }, 10000); }) window.addEventListener('beforeunload', function () { // 网页正常退出后,将埋入标志,设置成 true,表示正常退出 localStorage.setItem('good_exit', 'true'); })复制代码
上面是在
第二次
进入页面才知道网页崩溃,那么有什么方法可以在网页崩溃之后就可以上报呢其生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)
1、注册 service worker js 2、 每间隔 10 秒,就向 worker 发送消息 消息中包含: { type 字段:active 表示正常活跃,exit 表示正常退出 time 字段:表示当前时间 } 在页面要退出后,发送 type: exit,表示正常退出 3、 worker 内部注册有消息接收事件,接收页面发送过来的消息 接收到 type 为 active,表示正常活跃,更新内部 time 的值 接收到 type 为 exit,表示正常退出,更新内部 time 值为 0 worker 内部维护一个状态对象,包含时间,值为页面发送过来 每隔 15 秒检查一次,若时间与上一次没有改变,则说明页面可能崩溃(注意区分 time 为 0 的情况)复制代码
可以使用 Service Worker 来进行监控
网页加载后,埋入一个标志,表示正在加载
Script Error
跨域脚本的错误信息,因为处于保护信息的原因,只会展示 Script Error, 通过以下方式解决
1、外链脚本增加 crossorigin 属性:<script crossorigin src="http://other-domain.js"></script> 2、同时脚本的 Access-Control-Allow-Origin: 设置为 * 或者 当前域名复制代码
iframe Error
<iframe src="./test.html"></iframe> <script> window.frames[0].onerror = function(msg, source, lineno, colno,error) { console.log('frames onerror :>> ', msg,source,lineno,colno,error) } </script>复制代码
vue 自身 try catch 处理了错误,导致我们无法捕获
使用 Vue 提供的 errorHandler 方法捕获
Vue.config.errorHandler = function(err, vm, info) { let { message, // 异常信息 name, // 异常名称 script, // 异常脚本url line, // 异常行号 column, // 异常列号 stack // 异常堆栈信息 } = err // vm 为抛出异常的 Vue 实例 // info 为 Vue 特定的错误信息,比如错误所在的生命周期钩子 console.log('vue err,vm,info :>> ', err, vm, info) }复制代码
所以捕获错误总结下来:
// 1、try catch 中的 catch 错误捕获 可以在 webpack 打包时候,使用 AST 方式解析并在 catch 中插入日志上报代码 // 2、error 事件 window.addEventListener( 'error', error => { let data = {} // 此处与上面的 onerror 会重复事件 let { colno, lineno, message, filename, error, stack } = error //不一定所有浏览器都支持 colno 参数 let col = colno || (window.event && window.event.errorCharacter) || 0 data.url = url data.line = line data.col = col if (!!stack){ //如果浏览器有堆栈信息 //直接使用 data.msg = stack.toString() }else if (!!arguments.callee){ //尝试通过callee拿堆栈信息 let ext = [] let f = arguments.callee.caller, c = 3 //这里只拿三层堆栈信息 while (f && (--c>0)) { ext.push(f.toString()) if (f === f.caller) { break //如果有环 } f = f.caller } ext = ext.join(",") data.msg = ext } console.log('3、addEventListener error 捕获阶段>>>异常:', data) }, true ) // 3、unhandledrejection window.addEventListener('unhandledrejection', function(e) { // e.preventDefault(); // 阻止异常向上抛出 console.log('4、promise 异常 unhandledrejection :', e) }) // 4、errorHandler Vue.config.errorHandler = function(err, vm, info) { // vm 为抛出异常的 Vue 实例 // info 为 Vue 特定的错误信息,比如错误所在的生命周期钩子 let { message, // 异常信息 name, // 异常名称 script, // 异常脚本url line, // 异常行号 column, // 异常列号 stack // 异常堆栈信息 } = err console.log('2、vue errorHandler :>> ', err, vm, info) }复制代码
二、错误日志上报
既然异常已经捕获到了,那我们怎么处理呢,如何上报,需要上报哪些内容?
日志分类
1、一般日志分类等级
log、debug、info、warn、error复制代码
2、分场景使用日志上报类型
log: 记录流程信息 debug: 记录调试关键信息 info: 记录业务功能点,是否触发成功或者失败 warn: 页面警告信息 error: 页面错误或者业务异常信息复制代码
3、日志上报信息附带信息
1、用户id、session、用户名 2、当前错误信息 3、可以用来重现、推断当前错误发生的信息 4、上报时间 5、日志等级等等复制代码
4、日志上报策略
1、达量上传,设置一个缓存数量,到达即上报。因为不能一发生错误就要上报,会影响用户的网络。 2、日志埋点处各自有各自的上报等级。 需要有一个总配置地方,配置当前的上报等级 这样各处埋点可以判断当前需要上报的日志等级,等级小于设置值的话,不可上报。 3、本地缓存若暂存过多,需要删除前面的数据 4、抽样上报 5、设置缓存有效时间 等等复制代码
上报之后,接下来的步骤就是在服务端收集分析归类展示,基于badjs我们搭建一整套日志解析系统
三、日志上报
badjs 服务安装
1、前期预备工作
为了快速搭建,我们统一使用 docker 安装
备注:windows 环境使用 docker,需要安装 Docker Desktop
mysql 安装docker 安装 mysql
备注:mysql 安装好后,需要从 badjs-web (需要先把项目下载下来)项目中的 db 目录下。 使用
create.sql
初始化 web 相关的数据库mongodb 安装(不可设置密码)docker 安装 mongo
2、项目安装
github 克隆项目到本地
git clone https://github.com/BetterJS/badjs-installer复制代码
子项目下载以及依赖安装
// 克隆下载 badjs-acceptor、badjs-mq、badjs-storage、badjs-web 项目 yarn clone // 安装各项目的依赖 yarn install复制代码
3、修改配置项
修改 badjs-acceptor 项目的 project.debug.json/project.json
注意:这里是日志上报的地方,客户端初始化 badjs-report 时候需要设置的 url 属性即是这里的服务地址
http://{badjs-acceptor:port}/badjs
// 修改 port 属性: 从 80 改为 8083; // 因为 node 默认没有 80 端口的权限,需要你使用管理员权限才可以使用 { "port": 8083 }复制代码
修改 badjs-web 项目的 project.debug.json/project.json
// 修改 mysql 属性,配置我们 docker 安装好的 mysql 用户密码与端口 { "mysql" : { "url" : "mysql://root:123456@localhost:3306/badjs" } }复制代码
4、启动项目
yarn start
查看 badjs-web 的启动端口,访问 http://localhost:port 可以看到日志后台管理服务页面
四、badjs各模块
1、badjs-acceptor 接受客户端上报的日志
badjs-acceptor 收到日志上报,发送到 badjs-mq package.json 中配置 dispatcher 分发属性, 表示向 badjs-mq 请求的信息:如请求端口(10001)复制代码
2、badjs-mq 消息队列,保证消息有序稳定被接受
badjs-mq 接收 badjs-acceptor 的请求 package.json 中配置 acceptor 接收属性, 表示用来接收信息所配置的接口信息:如端口(10001)。 badjs-mq 再分发到 badjs-storage package.json 中配置 dispatcher 分发属性, 表示向 badjs-storage 请求的信息:如端口(10000)复制代码
3、badjs-storage 存储模块
badjs-storage: 接收来自 badjs-mq 的请求,再写入到 mongodb package.json 中配置 acceptor 接收属性, 表示用来接收信息所配置的接口信息:如端口(10000)复制代码
4、badjs-web 日志后台管理系统
badjs-web 查询日志存储,分类查看日志信息,解析日志内容 package.json 中配置 acceptor 接收属性, badjs-acceptor 可请求的端口 package.json 中配置 storage 存储属性, 查询 mongodb 数据 package.json 中配置 mysql 数据库属性, 查询 mysql 数据等等复制代码
五、上报日志插件 badjs-report
badjs-report 重写了 window.onerror 来捕获错误
1、安装
yarn add badjs-report复制代码
2、初始化
import badjs from 'badjs-report' badjs.init({ // 必须配置项 id: 1 // 此 id 为 badjs-web 启动后,申请的项目的 id,上报的日志根据该 id 区分业务模块 url: 'http://badjs-acceptor启动后的地址', // 日志上报到的地方 // 选择配置 uin: 123, // 指定用户的 id (该插件默认读取 qq uin) delay: 1000, // 延迟多少毫秒,合并缓冲区中的上报(默认 1000) ignore: [/Script error/i], // 忽略某个错误,遇到该错误不进行上报 random: 1, // 抽样上报, 值可以设置 0-1 之间。1 表示 100% 上报(默认为 1) repeat: 5, // 重复上报次数(对于同一个错误超过多少次不上报;避免单个用户同一错误上报过多的情况) onReport: function(id, errObj) {}, // 上报日志之后的回调。id 为上报的 id,errorObj 为上报的错误对象 submit: function(url) {}, // 覆盖原来的上报方式,原来是使用 new Image() 形式上报,可以修改成自己想要上报的方式,比如使用 post 内部构造好的 url ext: {}, // 扩展属性,后端做扩展处理属性。设置了 ext 的值,就会作为 'ext=设置的值' 合并到构造好的上报 url 中 offlineLog: false, // 是否开启离线日志(默认不开启为 false) offlineLogExp: 5 // 离线有效时间(默认最近5天) })复制代码
3、手动上报
a、badjs.report('error msg') b、badjs.report({ msg: 'error msg', // 需要上报的错误信息 target: 'error.js', // 发生错误的 js 文件 rowNum: 1, // 发生错误的行数 colNum: 2 // 发生错误的列数 })复制代码
4、延迟上报
暂存
badjs.push('error msg') badjs.push({ msg: 'error msg', // 需要上报的错误信息 target: 'error.js', // 发生错误的 js 文件 rowNum: 1, // 发生错误的行数 colNum: 2 // 发生错误的列数 })复制代码
立即上报
badjs.report({ msg: 'error msg', // 需要上报的错误信息 target: 'error.js', // 发生错误的 js 文件 rowNum: 1, // 发生错误的行数 colNum: 2 // 发生错误的列数 })复制代码
5、上报离线日志
badjs.reportOfflineLog()复制代码
六、项目应用
import BJ_REPORT from 'badjs-report' import Vue from 'vue' // 环境ID枚举 let ENV_ID_ENUM = { DEV: 1, SIT: 2, UAT: 3, PRO: 4 } let curEnv = '', origin = window.location.origin if (origin.indexOf('dev.xxx.com') > -1) { // http://dev.xxx.com // DEV 环境 curEnv = 'DEV' } else if (origin.indexOf('sit.xxx.com') > -1) { // http://sit.xxx.com // SIT 环境 curEnv = 'SIT' } else if (origin.indexOf('uat.xxx.com') > -1) { // http://uat.xxx.com // UAT 环境 curEnv = 'UAT' } else if (origin.indexOf('m.xxx.com') > -1) { // http://pro.xxx.com // PRO 环境 curEnv = 'PRO' } let envID = ENV_ID_ENUM[curEnv] || ENV_ID_ENUM.DEV // 初始化日志上报插件 BJ_REPORT.init({ id: envID, // 不指定 id 将不上报, url: 'http://{badjs-acceptor:port}/badjs' }) // 初始化项目时,可以暴露一个全局的 vm 实例,方便上传需要的信息 window.rootvm = new Vue({}) // 初始化项目 // 初始化监听异常 init(window.rootvm) function init(rootInstance) { Vue.config.errorHandler = function(err, curInstance, info) { // vm 为抛出异常的 Vue 实例 // info 为 Vue 特定的错误信息,比如错误所在的生命周期钩子 let { message, // 异常信息 // name, // 异常名称 // script, // 异常脚本url line, // 异常行号 column, // 异常列号 stack // 异常堆栈信息 } = err log(message, stack, line, column, curInstance) console.log('vue errorHandler :>> ', err, curInstance, info) } window.addEventListener( 'error', e => { let { colno, lineno, message, filename } = e log(message, filename, lineno, colno, rootInstance) console.log('addEventListener error 捕获阶段>>>异常:', e) }, true ) window.addEventListener('unhandledrejection', function(e) { // e.preventDefault(); // 阻止异常向上抛出 let { reason } = e log(JSON.stringify(reason), '', '', '', rootInstance) console.log('Promise 异常 unhandledrejection :', e) }) } function log(msg, target, rowNum, colNums, vminstance) { let msgs = `***[${msg}]***`, state = (vminstance && vminstance.$store && vminstance.$store.state) || {} // 用户信息 if (state.userInfo) { let { phoneNumber, userId } = state.userInfo msgs += `***[phone:${phoneNumber}--userId:${userId}]***` } // 路由信息 let { name, fullPath } = (vminstance && vminstance.$route) || {} msgs += `***[router-name: ${name} -- router-fullpath: ${fullPath}]***` BJ_REPORT.report({ msg: msgs, target, rowNum, colNums }) }
作者:码上起飞
链接:https://juejin.cn/post/7062657609592995847