如何监控前端异常?
一个完善的异常监控平台,是一个非常复杂的项目。它需要包括异常监控、信息收集、信息归类、信息的统计分析、异常场景的重现、异常在源码中的定位等等,同时,也要考虑异常日志的存放以及服务器压力等问题。
本文只涉及异常的捕获、上传方面的内容,大致围绕下面几点展开讨论:
- 前端需要处理的异常
- 前端异常的捕获方式
- 异常信息的上报方式
- 异常监控常见问题
为什么要处理异常?
- 增强用户体验
- 远程定位问题
- 未雨绸缪,及早发现问题
- 无法复线问题,尤其是移动端,机型,系统都是问题
- 完善的前端方案,前端监控系统;
对于 JS
而言,异常的出现不会直接导致 JS
引擎崩溃,最多只会使当前执行的任务终止。
前端需要处理的异常
- 语法错误
- 运行时异常
- EvalError eval错误
- RangeError 范围错误
- ReferenceError 引用错误
- TypeError 类型错误
- URIError URI错误
- SyntaxError 语法错误
- Error 通用错误
- 资源加载异常
- img
- script
- link
- audio
- video
- iframe
- @font-face
- 外链资源的DOM元素。。。
- Promise 异常
- 异步请求异常
- XMLHttpRequest
- fetch
前端异常的捕获方式
- try-catch-finally
- window.onerror = function () {}
- img
- script
- link
- window.addEventListener('error', function () {}, true)
- window.addEventListener("unhandledrejection", function () {})
- Promise.then().catch(function () {})
- 封装 XMLHttpRequest、fetch,覆写请求接口对象
try-catch-finally
try-catch
处理异常的能力有限,只能捕获捉到运行时非异步错误,对于语法错误(如:中文分号)和异步错误(如:回调、promise、setTimeout )就显得无能为力。
try {
// 模拟一段可能有错误的代码
throw new Error("会有错误的代码块")
} catch(e){
// 捕获到try中代码块的错误得到一个错误对象e,进行处理分析
report(e)
} finally {
console.log("finally")
}
window.onerror
window.onerror
是一个全局变量,默认值为null。当 JS
运行时错误(包括语法错误)发生时,window
会触发一个 ErrorEvent
接口的事件,并执行 window.onerror();
。onerror
可以接受多个参数。
window.onerror
无法捕获 静态资源异常、接口异常。
注意:语法错误会导致出现语法错误的那个脚本块执行失败,所以语法错误会导致当前代码块运行终止,从而导致整个程序运行中断,如果语法错误这个发生在我们的错误监控语句块中,那么我们就什么也监控不到了。
window.onerror = function (msg, url, row, col, error) {
// msg:错误信息(字符串)。
// url:发生错误的脚本URL(字符串)
// row:发生错误的行号(数字)
// col:发生错误的列号(数字)
// error:Error对象(对象)
console.log('我知道异步错误了');
console.log({
msg, url, row, col, error
})
return true;
};
window.onerror = function () {}
要比其他脚本先执行(注意这个前提!),才可以捕捉到语法错误。
window.onerror
函数只有在返回 true 的时候,异常才不会向上抛出,否则即使是知道异常的发生控制台还是会显示Uncaught Error: xxxxx
。
window.addEventListener
监听 js 运行时错误事件,会比 window.onerror
先触发,与 onerror
的功能大体类似,不过事件回调函数传参只有一个保存所有错误信息的参数,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误。
当资源(如img或script)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的 onerror() 处理函数。这些error事件不会向上冒泡到window,但可以在捕获阶段被捕获。因此如果要全局监听资源加载错误,需要在捕获阶段捕获事件
网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP
的状态是 404
还是其他比如 500
等等,所以还需要配合服务端日志才进行排查分析才可以。
需要注意:
- 不同浏览器下返回的
error
对象可能不同,需要注意兼容处理。 - 需要注意避免
addEventListener
重复监听。
window.addEventListener('error', (msg, url, row, col, error) => {
console.log('我知道 404 错误了')
console.log(
msg, url, row, col, error
);
return true
}, true)
unhandledrejection
没有写 catch
的 Promise
中抛出的错误无法被 onerror
或 try-catch
捕获到,所以我们务必要在 Promise
中不要忘记写 catch
处理抛出的异常。
为了防止有漏掉的 Promise
异常,在全局增加一个对 unhandledrejection
的监听,用来全局监听Uncaught Promise Error
。
window.addEventListener("unhandledrejection", function(e){
// Event新增属性
// @prop {Promise} promise - 状态为rejected的Promise实例
// @prop {String|Object} reason - 异常信息或rejected的内容
// 会阻止异常继续抛出,不让Uncaught(in promise) Error产生
e.preventDefault()
})
Promise.then().catch(function () {})
new Promise(function(resolve, reject) {
throw 'Uncaught Exception!';
}).catch(function(e) {
console.log(e); // Uncaught Exception!
})
封装XMLHttpRequest、fetch
/**
* 函数:封装XMLHttpRequest和fetch对象,获取、上传异常信息。
*/
function captureRequestError (reportLog) {
// 覆写XMLHttpRequest API
if(window.XMLHttpRequest) {
var xmlhttp = window.XMLHttpRequest
var _oldSend = xmlhttp.prototype.send
var _handleEvent = function (event) {
if (event && event.toString() === "[object ProgressEvent]" && event.currentTarget && event.currentTarget.status !== 200) {
//处理错误信息
}
}
xmlhttp.prototype.send = function () {
if (this['addEventListener']) {
this['addEventListener']('error', _handleEvent)
this['addEventListener']('load', _handleEvent)
this['addEventListener']('abort', _handleEvent)
this['addEventListener']('close', _handleEvent)
} else {
var _oldStateChange = this['onreadystatechange']
this['onreadystatechange'] = function (event) {
if (this.readyState === 4) {
_handleEvent(event)
}
_oldStateChange && _oldStateChange.apply(this, arguments)
}
}
return _oldSend.apply(this, arguments)
}
}
//覆写fetch API
if (window.fetch) {
var _oldFetch = window.fetch
window.fetch = function() {
return _oldFetch.apply(this, arguments).then(function(res){
// 处理信息
return res
}).catch(function(error){
// 处理信息
})
}
}
}
VUE errorHandler
vue本身有监听异常机制,我们可以在它提供的监听函数中,上传异常信息。
Vue.config.errorHandler = (err, vm, info) => {
console.error('通过vue errorHandler捕获的错误');
console.error(err);
console.error(vm);
console.error(info);
}
崩溃和卡顿
卡顿也就是网页暂时响应比较慢, JS
可能无法及时执行。但崩溃就不一样了,网页都崩溃了,JS
都不运行了,还有什么办法可以监控网页的崩溃,并将网页崩溃上报呢?
利用 window
对象的 load
和 beforeunload
事件实现了网页崩溃的监控。
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');
});
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'));
}
以使用 Service Worker
来实现网页崩溃的监控:
Service Worker
有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker
一般情况下不会崩溃;Service Worker
生命周期一般要比网页还要长,可以用来监控网页的状态;- 网页可以通过
navigator.serviceWorker.controller.postMessage API
向掌管自己的SW
发送消息。
异常日记上报方式
异步请求上报, 后端提供接口,或者直接发到日志服务器
img请求上报,url参数带上错误信息
function report(error) {
var reportUrl = 'http://xxxx/report';
new Image().src = reportUrl + 'error=' + error;
}
Sentry使用
sentry 中文翻译是哨兵。它是一个款错误跟踪、性能监控工具。
Sentry 是一个开源的实时错误报告工具,支持 web 前后端、移动应用以及游戏,支持多种语言(JavaScript、Java、Go、Nodejs、Php、Python 等)和框架(React、Vue、Angular、Next.js 等),还提供了 GitHub、Slack、Trello 等常见开发工具的集成。
使用 sentry 需要结合两个部分:客户端与服务端。客户端就是你需要去监听的项目。而服务端就是一个数据管理平台,它会展示已收集到的错误信息和项目信息,并支持项目管理,组员管理、邮件报警等功能。
可以直接使用 sentry 官方平台,也可以利用 Sentry 的开源库在自己的服务器上搭建服务,官方已经提供了完善的操作文档。 Sentry 的搭建方式主要有两种:Python 安装、通过 Docker 安装。由于 Docker 更加方便易控,官方推荐 Docker 部署。
这里,我们以官方平台为例,简单讲一下接入步骤:
注册账号: 登录入 sentry 官网,注册一个账号。(注意翻墙,shadowsocks PAC 自动模式可能无效,请添加 【PAC 用户自定义规则】或者启用全局模式)。
创建项目,获取DSN: 注册后,在官网创建一个项目,后台会自动生成一个 DSN。DSN 是一个重要的值,用来告诉客户端将事件发送到哪里。
客户端监听项目: 通过 CDN 或者 npm 引入 sentry。
以 npm 引入为例:
import Vue from 'vue' import * as Sentry from "@sentry/vue" Sentry.init({ Vue, dsn: "https://xxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxx", integrations: [], // Set tracesSampleRate to 1.0 to capture 100% // of transactions for performance monitoring. // We recommend adjusting this value in production tracesSampleRate: 1.0, // defaultIntegrations: true })
客户端集成:这是客户端的核心部分,它告诉 sentry 怎么收集、收集哪些错误。
Sentry 默认情况下启用系统集成以集成到标准库或解释器本身。
defaultIntegrations 用来表示是否使用默认添加的集成。
integrations 用来标识启用集成的名称列表。列表应该包含所有启用的集成,包括默认的集成。包含默认集成是因为不同的 SDK 版本可能包含不同的默认集成。
integrations 可用来删除、或添加集成:
import { ReportingObserver } from "@sentry/integrations" Sentry.init({ dsn: "https://xxxxxxxxxxxxxxxxxxx@xxxxxxx.ingest.sentry.io/xxxxx", integrations: [new ReportingObserver()] })
除引用 sentry 提供的集成,我们可以自定义一个集成,还可以主动捕获并上报错误。
主动上报的方式有两种: 一种是直接上报文本信息,参数为一个字符串;另一种是上报错误对象,参数为一个 error 对象或者类对象。
Sentry.captureMessage('error-message', 'fatal') try { console.log(a) } catch (error) { Sentry.captureException(error) }
以下只是简单的使用示例,详细使用可查看下面链接:
https://docs.sentry.io/platforms/javascript/
Sentry For Vue 完整接入详解(2021 Sentry v21.8.x)前方高能预警!三万字,慎入!
常见问题
跨域脚本异常报错信息
生产环境的 JS 做静态资源 CDN 化,导致访问的页面跟脚本文件来自不同的域名,这时候如果没有进行额外的配置,就会容易产生 Script error
。
Script error
是浏览器在同源策略限制下产生的,浏览器处于对安全性上的考虑,当页面引用非同域名外部脚本文件时中抛出异常的话,此时本页面是没有权利知道这个报错信息的,取而代之的是输出 Script error
这样的信息。
在H5的规定中,只要满足下面两个条件,是允许获取跨源脚本的错误信息的。
- 客户端在 script 标签上增加 crossorigin 属性;
- 服务端设置 js 资源响应头
Access-Control-Origin: 指定域名 | *
。
window.error 和 window.addEventListener区别
window.onerror
含有详细的 error
信息(如:error.stack),而且兼容性更好,所以一般 JS 运行时错误使用 window.onerror
捕获处理。
window.addEventListener('error')
,可以捕获 JS 运行时的错误,也能捕获资源加载错误。为避免重复上报 js 运行时错误,此时应该只有event.srcElement inatanceof HTMLScriptElement、HTMLLinkElement、HTMLImageElement
时才上报
总结
使用场景分析
- 可疑区域增加
Try-Catch
- 全局监控
JS
异常window.onerror
- 全局监控静态资源异常
window.addEventListener
- 捕获没有
Catch
的Promise
异常:unhandledrejection
- vue框架:
VUE errorHandler
、React 框架:React componentDidCatch
- 监控网页崩溃:
window
对象的load
和beforeunload
业界已经有的监控平台
- Sentry 开源 (推荐)
- 阿里的 ARMS
- fundebug
- FrontJS
另外还有一些轻量级的 BetterJS