总结:这三年沉淀的前端错误监控系统,一篇文章讲透给你
优采云 发布时间: 2022-11-20 20:25总结:这三年沉淀的前端错误监控系统,一篇文章讲透给你
// 控制台运行<br />fetch('/remote/notdefined', {})<br />复制代码<br />
错误
所有的原因都来自错误,那么我们如何捕获错误。
试着抓
可以捕获常规运行时错误,语法错误和异步错误不能
// 常规运行时错误,可以捕获 ✅<br />try {<br /> console.log(notdefined);<br />} catch(e) {<br /> console.log('捕获到异常:', e);<br />}<br /><br />// 语法错误,不能捕获 ❌<br />try {<br /> const notdefined,<br />} catch(e) {<br /> console.log('捕获到异常:', e);<br />}<br /><br />// 异步错误,不能捕获 ❌<br />try {<br /> setTimeout(() => {<br /> console.log(notdefined);<br /> }, 0)<br />} catch(e) {<br /> console.log('捕获到异常:',e);<br />}<br />复制代码<br />
try/catch的优点是处理细心,但缺点也很明显。
window.onerror 错误
纯js错误集合,window.onerror,当发生JS运行时错误时,window会触发ErrorEvent接口的错误事件。
/**<br />* @param {String} message 错误信息<br />* @param {String} source 出错文件<br />* @param {Number} lineno 行号<br />* @param {Number} colno 列号<br />* @param {Object} error Error对象<br />*/<br /><br />window.onerror = function(message, source, lineno, colno, error) {<br /> console.log('捕获到异常:', {message, source, lineno, colno, error});<br />}<br />复制代码<br />
先验证接下来的几个错误能不能被捕获。
// 常规运行时错误,可以捕获 ✅<br /><br />window.onerror = function(message, source, lineno, colno, error) {<br /> console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />console.log(notdefined);<br /><br />// 语法错误,不能捕获 ❌<br />window.onerror = function(message, source, lineno, colno, error) {<br /> console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />const notdefined,<br /> <br />// 异步错误,可以捕获 ✅<br />window.onerror = function(message, source, lineno, colno, error) {<br /> console.log('捕获到异常:',{message, source, lineno, colno, error});<br />}<br />setTimeout(() => {<br /> console.log(notdefined);<br />}, 0)<br /><br />// 资源错误,不能捕获 ❌<br /><br /> window.onerror = function(message, source, lineno, colno, error) {<br /> console.log('捕获到异常:',{message, source, lineno, colno, error});<br /> return true;<br />}<br /><br />/yun.tuia.cn/image/kkk.png"><br />复制代码<br />
window.onerror 抓不到资源错误怎么办?
窗户。添加事件*敏*感*词*器
当资源(如图片或脚本)加载失败时,加载该资源的元素会触发Event接口的错误事件。这些错误事件不会冒泡到窗口,但可以被捕获。而window.onerror无法*敏*感*词*和捕捉。
// 图片、script、css加载错误,都能被捕获 ✅<br /><br /> window.addEventListener('error', (error) => {<br /> console.log('捕获到异常:', error);<br /> }, true)<br /><br /><br /><br /><br /> <br />// new Image错误,不能捕获 ❌<br /><br /> window.addEventListener('error', (error) => {<br /> console.log('捕获到异常:', error);<br /> }, true)<br /><br /><br /> new Image().src = 'https://yun.tuia.cn/image/lll.png'<br /><br /><br />// fetch错误,不能捕获 ❌<br /><br /> window.addEventListener('error', (error) => {<br /> console.log('捕获到异常:', error);<br /> }, true)<br /><br /><br /> fetch('https://tuia.cn/test')<br /><br />复制代码<br />
new Image的使用比较少,自己处理错误即可。
但是一般的获取呢?Fetch 返回 Promise,但无法捕获 Promise 错误。我们应该做什么?
承诺错误
常见的 Promise 错误
try/catch 无法捕获 Promise 中的错误
// try/catch 不能处理 JSON.parse 的错误,因为它在 Promise 中<br />try {<br /> new Promise((resolve,reject) => { <br /> JSON.parse('')<br /> resolve();<br /> })<br />} catch(err) {<br /> console.error('in try catch', err)<br />}<br /><br />// 需要使用catch方法<br />new Promise((resolve,reject) => { <br /> JSON.parse('')<br /> resolve();<br />}).catch(err => {<br /> console.log('in catch fn', err)<br />})<br />复制代码<br />
异步错误
try/catch 无法捕获异步包装的错误
const getJSON = async () => {<br /> throw new Error('inner error')<br />}<br /><br />// 通过try/catch处理<br />const makeRequest = async () => {<br /> try {<br /> // 捕获不到<br /> JSON.parse(getJSON());<br /> } catch (err) {<br /> console.log('outer', err);<br /> }<br />};<br /><br />try {<br /> // try/catch不到<br /> makeRequest()<br />} catch(err) {<br /> console.error('in try catch', err)<br />}<br /><br />try {<br /> // 需要await,才能捕获到<br /> await makeRequest()<br />} catch(err) {<br /> console.error('in try catch', err)<br />}<br />复制代码<br />
导入块错误
import实际上返回的是一个promise,所以使用下面两个方法来捕获错误
// Promise catch方法<br />import(/* webpackChunkName: "incentive" */'./index').then(module => {<br /> module.default()<br />}).catch((err) => {<br /> console.error('in catch fn', err)<br />})<br /><br />// await 方法,try catch<br />try {<br /> const module = await import(/* webpackChunkName: "incentive" */'./index');<br /> module.default()<br />} catch(err) {<br /> console.error('in try catch', err)<br />}<br />复制代码<br />
总结:全局捕获Promise中的错误
以上三种其实归结为Promise类型的错误,可以通过unhandledrejection捕获
// 全局统一处理Promisewindow.addEventListener("unhandledrejection", function(e){ console.log('捕获到异常:', e);});fetch('https://tuia.cn/test')复制代码<br />
为了防止遗漏Promise异常,可以使用unhandledrejection全局监控Uncaught Promise Error。
视图错误
由于Vue会捕获所有Vue单文件组件或继承自Vue.extend的代码,因此Vue中发生的错误不会被window.onerror直接捕获,而是抛给Vue.config.errorHandler。
/** * 全局捕获Vue错误,直接扔出给onerror处理 */Vue.config.errorHandler = function (err) { setTimeout(() => { throw err })}复制代码<br />
反应错误
React通过componentDidCatch声明一个边界错误的组件
class ErrorBoundary extends React.Component {<br /> constructor(props) {<br /> super(props);<br /> this.state = { hasError: false };<br /> }<br /><br /> static getDerivedStateFromError(error) {<br /> // 更新 state 使下一次渲染能够显示降级后的 UI<br /> return { hasError: true };<br /> }<br /><br /> componentDidCatch(error, errorInfo) {<br /> // 你同样可以将错误日志上报给服务器<br /> logErrorToMyService(error, errorInfo);<br /> }<br /><br /> render() {<br /> if (this.state.hasError) {<br /> // 你可以自定义降级后的 UI 并渲染<br /> return Something went wrong.;<br /> }<br /><br /> return this.props.children; <br /> }<br />}<br /><br />class App extends React.Component {<br /> <br /> render() {<br /> return (<br /> <br /> <br /> <br /> )<br /> }<br />}<br />复制代码<br />
但是错误边界不会捕获以下错误:React 事件处理、异步代码、错误边界本身抛出的错误。
跨域问题
一般情况下,如果出现Script error之类的错误,基本可以确定是存在跨域问题。
如果当前服务页面和云JS位于不同的域名,云JS出错,window.onerror会显示Script Error。可以通过以下两种方法解决。
const script = document.createElement('script');script.crossOrigin = 'anonymous';script.src = 'http://yun.tuia.cn/test.js';document.body.appendChild(script);复制代码<br />
<br /><br /><br /> Test page in http://test.com<br /><br /><br /> <br /> <br /> window.onerror = function (message, url, line, column, error) {<br /> console.log(message, url, line, column, error);<br /> }<br /><br /> try {<br /> foo(); // 调用testerror.js中定义的foo方法<br /> } catch (e) {<br /> throw e;<br /> }<br /> <br /><br /><br />复制代码<br />
你会发现如果不加try catch,console.log会打印script error。可以通过添加try catch来捕获。
我们来看看现场。一般是调用remote js,常见的有以下三种情况。
调用方法场景
通过封装一个函数,可以对原来的方法进行修饰,使其可以被try/catch。
<br /><br /><br /> Test page in http://test.com<br /><br /><br /> <br /> <br /> window.onerror = function (message, url, line, column, error) {<br /> console.log(message, url, line, column, error);<br /> }<br /><br /> function wrapErrors(fn) {<br /> // don't wrap function more than once<br /> if (!fn.__wrapped__) {<br /> fn.__wrapped__ = function () {<br /> try {<br /> return fn.apply(this, arguments);<br /> } catch (e) {<br /> throw e; // re-throw the error<br /> }<br /> };<br /> }<br /><br /> return fn.__wrapped__;<br /> }<br /><br /> wrapErrors(foo)()<br /> <br /><br /><br /><br />复制代码<br />
可以尝试去掉wrapErrors感受一下。
活动现场
本机方法可以被劫持。
" />
<br /><br /><br /> Test page in http://test.com<br /><br /><br /> <br /> const originAddEventListener = EventTarget.prototype.addEventListener;<br /> EventTarget.prototype.addEventListener = function (type, listener, options) {<br /> const wrappedListener = function (...args) {<br /> try {<br /> return listener.apply(this, args);<br /> }<br /> catch (err) {<br /> throw err;<br /> }<br /> }<br /> return originAddEventListener.call(this, type, wrappedListener, options);<br /> }<br /> <br /> http://test.com<br /> <br /> <br /> window.onerror = function (message, url, line, column, error) {<br /> console.log(message, url, line, column, error);<br /> }<br /> <br /><br /><br />复制代码<br />
大家可以尝试去掉封装EventTarget.prototype.addEventListener的代码感受一下。
报告界面
为什么不能直接使用GET/POST/HEAD请求接口上报?
这个比较容易想到。一般来说,托管域名不是当前域名,所以所有的接口请求都会构成跨域。
为什么请求其他文件资源(js/css/ttf)不能报错?
资源节点创建后,只有将对象注入浏览器DOM树后,浏览器才会真正发送资源请求。而且加载js/css资源也会阻塞页面渲染,影响用户体验。
不仅不需要在构造图片中插入DOM,只要在js中创建一个Image对象就可以发起请求,不存在阻塞问题。在没有js的浏览器环境下,也可以通过img标签正常管理。
使用新的图像进行界面报告。最后一题也是一张图。报告时,选择了 1x1 透明 GIF 而不是其他 PNG/JEPG/BMP 文件。
首先,1x1 像素是最小的合法图像。而且,因为是通过图片来完成的,所以图片应该是透明的,这样才不会影响页面本身的显示效果。两者表示图片是透明的,只要用一个二进制位来标记图片为透明色,就不需要存储颜色空间数据,可以节省体积。因为需要透明色,所以可以直接排除JPEG。
对于同样的响应,GIF 比 BMP 可以节省 41% 的流量,比 PNG 可以节省 35% 的流量。GIF 是必经之路。
使用 1*1 gif
非阻塞加载
尽量避免SDK js资源加载的影响。
通过先缓存window.onerror的错误记录,然后异步加载SDK,再在SDK中处理报错。
<br /><br /><br /> <br /> (function(w) {<br /> w._error_storage_ = [];<br /> function errorhandler(){<br /> // 用于记录当前的错误 <br /> w._error_storage_&&w._error_storage_.push([].slice.call(arguments));<br /> } <br /> w.addEventListener && w.addEventListener("error", errorhandler, true);<br /> var times = 3,<br /> appendScript = function appendScript() {<br /> var sc = document.createElement("script");<br /> sc.async = !0,<br /> sc.src = './build/skyeye.js', // 取决于你存放的位置<br /> sc.crossOrigin = "anonymous",<br /> sc.onerror = function() {<br /> times--,<br /> times > 0 && setTimeout(appendScript, 1500)<br /> },<br /> document.head && document.head.appendChild(sc);<br /> };<br /> setTimeout(appendScript, 1500);<br /> })(window);<br /> <br /><br /><br /> 这是一个测试页面(new)<br /><br /><br />复制代码<br />
采集聚合端(日志服务器)
在这个环节中,输入是借口收到的错误记录,输出是有效数据存储。核心功能需要清洗数据,顺便解决了过大的服务压力。另一个核心功能是存储数据。
整体流程可以看做是错误识别->错误过滤->错误接收->错误存储。
错误识别(SDK配合)
在聚合之前,我们需要具备识别不同维度错误的能力,可以理解为定位单个错误条目或者单个错误事件的能力。
单个错误条目 ****
通过日期和随机值生成对应的错误条目id。
<p>const errorKey = `${+new Date()}@${randomString(8)}`function randomString(len) { len = len || 32; let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; let maxPos = chars.length; let pwd = ''; for (let i = 0; i {<br /> setTimeout(() => {<br /> maxLimit = MAX_LIMIT;<br /> task();<br /> }, TICK);<br />};<br />task();<br /><br />const check = () => {<br /> if (maxLimit