前端日志原理
主要从下面几个方面进行分析
- 如何捕获错误信息并收集
- 如何设计友好的上传方案
怎样获取错误日志
- 浏览器提供了 try...catch 和 window.onerror,unhandledrejection帮助我们获取用户页面的脚本错误信息。
- 一般来说呢,使用 try...catch 可以捕获前端 JS 的运行时错误,同时拿到出错信息,例如错误信息描述、堆栈、行号、具体的出错文件信息等。 我们也可以在这个阶段将用户浏览器信息等静态内容一起记录下来,快速地定位问题发生的原因。需要注意的是, try...catch 无法捕获到语法错误,只能在单一的作用域捕获错误信息,如果是异步函数里面的内容,就需要把 function 函数块内容全部加入 try...catch 中执行。
javascript
try {
// 单一作用域 try...catch 可以捕获错误信息并进行处理
console.log(obj);
} catch(e) {
// 处理异常
console.log(e);
}
try {
// 不同作用域不能捕获到错误信息
setTimeout(function() {
console.log(obj);
}, 200);
} catch(e) {
// 处理异常
console.log(e);
}
setTimeout(function() {
try {
// 当前作用域 try...catch 可以捕获错误信息并进行处理
console.log(obj);
} catch(e) {
console.log(e);
}
}, 200);
- 上面的这个例子中,try...catch 无法获取异步函数 setTimeout 或其他作用域中的错误信息,这样就只能每个函数里面添加 try...catch 了。相比之下,window.onerror 的方法可以在任何执行的上下文中执行,如果 window 对象增加了一个错误处理函数,便既能处理捕获错误又能保持代码的优雅性。window.onerror 一般用于捕捉脚本语法错误和运行时错误,可以捕获出错的文件信息,如出错信息,出错文件,行号等,当前页面执行的所有 JS 脚本错误都会被捕获到。
javascript
window.onerror = function (msg, url, line) {
// 可以捕获异步函数中的错误信息并进行处理, 提示Script error
console.log(msg); //捕获错误信息
console.log(url); //捕获出错的文件路径
console.log(line); //捕获错误出错的行数
};
setTimeout(function () {
console.log(obj); // 可以被捕获到, 并且在 onerror 中处理
});
- 然而,使用 onerror 要注意,在不同的浏览器中实现函数处理返回的异常对象不是相同的,而且如果报错的 JS 和 HTML 不在同一个域名下,错误时,window.onerror 中的 errorMsg 全部为 script error 而不是具体的错误描述信息,此时需要添加 JS 脚本的跨域设置。
xml
<script src="/www.domain.com/main.js"></script>
unhandledrejection 异常监控处理
许多环境(例如 Node.js ) 默认情况下会向控制台打印未处理的 Promise rejections。您可以通过添加一个处理程序来防止 unhandledrejection 这种情况的发生,该处理程序除了您希望执行的任何其他任务之外,还可以调用 preventDefault() 来取消该事件,从而防止该事件冒泡并由运行时的日志代码处理。这种方法之所以有效,是因为 unhandledrejection 是可以取消的。
js
window.addEventListener('unhandledrejection', function (event) {
// ...您的代码可以处理未处理的拒绝...
// 防止默认处理(例如将错误输出到控制台)
event.preventDefault();
});
- 如果服务器因为一些原因不能设置跨域或设置起来比较麻烦,那就只能在每个引用的文件里面添加 try...catch 进行处理。
- 虽然使用window.onerror 可以捕获页面的出错信息,出错文件,行号,但是 window.onerror 有跨域限制,如果需要获取错误发生的具体描述,堆栈内容,行号,列号和具体的出错文件等详细日志,就必须使用 try...catch,但是try...catch 又不能在多个作用域中统一处理错误。
- 幸运的是,我们可以对前端脚本中常用的异步方法入口函数或模块引用的入口方法统一使用 try...catch 进行一层封装,这样就可以使用 try...catch 又不能在多个作用域中统一处理错误。
js
window.setTimeoutTry = function (fn, time) {
let args = arguments;
let _fn = function() {
try {
return fn.apply(this, args); // 将函数参数用 try...catch 包裹
} catch (e) {
console.log(e);
};
};
return window['setTimeout'](_fn, time);
};
try {
setTimeoutTry( function() {
obj // 这获取错误信息, ReferenceError: obj is not defined
}, 300)
} catch (e) {
console.log(e);
};
- 我们可以不同的作用域的 setTimeout 参数函数的引用方式使用 try...catch 进行封装,让 try...catch 能捕获到 setTimeout 脚本的错误并使用setTimeoutTry 函数来代替。对于异步引入模块定义函数require 或 define 也可以进行类似的封装,这样就可以捕获到不同模块里面作用域的错误信息了。因此,这里捕获错误的方式可以根据具体的条件和场景灵活选择,在没有特定限制情况下,使用 window.onerror 使比较高效,便捷的。
当资源加载失败或无法使用时,会在Window对象触发error事件。例如:script 执行时报错。
javascript
window.addEventListener('error', (event) => {
console.log(event)
});
页面崩溃收集
- 可以监听 window 对象的 load和beforeunload,并使用sessionStorage 对网页崩溃监控
js
window.addEventlistener('load', () => (
sesionStorage.setItem ('good exit', 'pending')
)
window.addEventlistener ('beforeunload', () => (
sesionStorage.setItem('good exit', 'true')
)
// 捕获页面崩溃
if (sesionStorage.getItem('good exit') && sesionStorage.getirtem cod exit') !== true )) {
}
- Service Worker 和网页的主线程相互独立,因此同样也可以监听页面崩溃
处理的错误或异常
- JavaScript语法错误、代码异常
- AJAX 请求异常 ( xhr.addEventListener(error,fuction (e) {})
- 静态资源加载异常
- Promise 异常
- 跨域脚本错误
- 页面崩溃
- 框架错误
性能数据和错误信息上报
数据都有了,那么该如何上报呢?可能有的开发者会想:"不就是一个 AAX求吗 "实没有这么简单,有一些细节需要考虑。
-
上报采用单独域名是否更好? 我们发现,成熟的网站数据上报的域名往往与业务域名并不相同。这样做的好处主要有以下两点
- 使用单独域名,可以防止对主业务服务器造成压力,能够避免日志相关处理逻辑和数据在主业务服务器上的堆积。
- 另外,很多浏览器对同一个域名的请求量有并发数的限制,单独域名能够充分利用现代浏览器的并发设置。
-
独立域名的跨域问题
对于单独的日志域名,肯定会涉及跨域问题。我们经常发现页面使用构造空的 Image 对象的方式进行数据上报。原因是请求图片并不涉及跨域的问题。
js
let url = 'xxxx'
let img = new Image()
img.src = ur
我们可以将数据进行序列化,作为 URL 参数进行传递,代码如下。
js
let url = 'xxx?data=' + JSON.stringify(data)
let img = new Image()
img.src=url
-
何时上报数据 页面加载性能数据可以在页面稳定后进行上报,一次上报就是一次访问,对于其他错误和异常数据的上报,假设我们的应用日志量很大,那么就有必要将日志合并,在同一时间统一上报。那么,在什么场景下上报性能数据呢? 一般有如下4种场景。
- 页面加载和重新刷新
- 页面切换路由
- 页面所在的 Tab 标签重新变得可见
- 页面关闭
但是,对于越来越多的单页应用来说,需要格外注意数据上报时机介绍完以上细节问题,我们来着重聊一聊单页应用上报。如果切换路由是通过改变 hash值来实现的,那么只需要监听 hashchange 事件;如果是通 history API 改变 URL 来实现的,那么需要使用 pushState 和 replaceState 事件。当然,一劳永逸的做法是进行打补丁(monkey patch),并结合发布/订阅模式为相关事件的触发添加处理。
jsconst patchMethod = type => () => { const result = history[type].apply(this, arguments) const event = new Event (type) event.arguments= arguments window.dispatchEvent (event) return result } history.pushstate = patchMethod('pushstate') history.replaceState = patchMethod ('replaceState')
以上代码通过重写 history.pushstate 和 history.replaceState 方法,添加并触发 pushSate和replaceState 事件,可以在 history.pushstate 和 history.replaceState 事件触发时添加订阅函数并进行上报
jswindow.addEventListener('replaceState', e => { // 上报数据 }) window.addEventListener ('pushstate', e => { // 上报数据 })
对于非单页面应用,该何时上报,以及如何上报呢?
如果是在页面离开时进行数据发送的,那么在页面卸载期间是否能够安全地发送完数据就是一个难题题,因为在页面跳转,进人下一个页面时,难以保证异步数据的安全发送。如果使用同步的 AJAX法,则代码如下所示。
jswindow.addEventListener ('unload', logData, false); const logData = () => { let client = new XMLHttpRequest() client.open("POST","/log",false) // 第三个参数表明XHR是同步 client.setRequestHeader("Content-Type","text/plain;charset-UrF-8") client.send(data) }
上述代码虽然可以完成数据的上报,但可能会影响页面跳转的流畅性和用户体验。这里给大家推荐一下 sendBeacon 方法,其代码如下。
jswindow.addEventlistener ('unload', logData, false) const logData =() => { navigator.sendBeacon("/log", data) }
navigator.sendBeacon 天生就是来解决网页跳转时的请求发送问题的。它的几个特点决定了对应的解决方案。
- 它的行为是异步的,也就是说请求的发送不会阻塞跳持到下一个页面,因此可以保证跳转的流畅度。
- 它在没有极端数据量和队列总数的限制下,会优先返回 true 以保证请求发送成功。
如果数据量小,可以采用动态创建 img 标签;如果 URL 过长,可以采用 sendBeacon 发送,这个时候要注意它的兼容性,如果浏览器不支持,可以采用 AJAX post 同步请求。
无侵入和性能友好的方案设计
下面针对这几个阶段聊一下关键方面的核心细节
-
数据上报优化
借助 HTTP2.0/ HTTP3.0 提供的高性能, 可以优化上报性能
-
接口和智能化设计
由于线上情况复杂多样,所以我们需要选择更加智能化的方案。关于这一点,我们可以从以下个方面考虑。
- 识别流量高峰和低谷时期,动态设置上报采样率
- 增强数据清洗能力,提高数据的可用性,对一些垃圾信息进行过滤
- 通过配置化,减少业务接入成本
- 如果用户一直触发错误,则系统会不停地上报相同的错误内容,这时可以考虑是否需要进行短时间滤重
-
实时性
目前,我们对系统数据的分析都是后置的,而如果想做到实时提醒,就要依赖后端服务,将超调值的情况进行邮件或短信发送。
怎样通过高效的方式来找到问题
- 为了方便查看收集到的这些信息,我们通常可以建立一些简单的内容管理系统来管理查看错误日志,对同一类型的错误做归并统计,也可以建立错误量实时统计来查看错误量的即时变化情况。当某个版本发布后,如果收到的错误量明显增加,就需要格外注意。
- 另外一点要注意的是,上报错误信息机制是用来辅助产品质量改进的,不能因为在页面中添加了错误信息收集和上报而影响原有的业务模块功能。