由于我比较擅长监控方面的建设,面试的时候也刚好被问到了,自然对于我来说不是问题,现重新整理分享出来,你按照这个能回答上80%左右,面试官印象分有了!
一、为什么要做监控
- 前端异常是指在用户使用系统过程中无法得到符合预期结果的情况,不同的异常带来的后果程度不同,轻则引起用户使用不悦,重则导致产品无法使用,使用户丧失对产品的认可。
- 为了能够让开发者可以快速的感知系统稳定性结果,异常监控是目前为止最有效的系统反馈机制。
二、异常分类
-
JS运行异常
- EvalError eval错误
- RangeError 范围错误
- ReferenceError 引用错误
- TypeError 类型错误
- URIError URI错误
- SyntaxError 语法错误
- Error 通用错误
-
资源加载异常
- 图片加载异常
- script加载异常
- link加载异常
-
promise异常
-
Ajax异常/fetch异常
- Ajax失败
- Ajax超时
-
浏览器奔溃异常
- 浏览器无法操作,进程卡死
- 内存泄露
-
浏览器卡顿异常
- FPS异常
三、异常采集方式
异常采集方式一般分为两种:全局接口捕获和单点异常捕获。
JS运行异常
//方法一
window.onerror = function() {}
//方法二
重写console.error();
//方法三
try {
...
} catch (e) {
handleError(e);
}
资源加载异常
window.addEventListener('error', (event)=> {
// 通过tagName区分不同资源
let tagName = event.tagName.toLocaleLowerCase();
if (tagName !== 'img' && tagName !== 'script' && tagName !== 'link') return;
if (tagName === 'link' && event.target.rel !== 'stylesheet') return;
// 上报异常信息
...
reportData('resourceError', event)
})
Promise异常
window.addEventListener('unhandledrejection', (event)=> {
let reason = err.reason;
// 过滤掉谷歌插件的报错
if (reason.stack.indexOf('chrome-extension') !== -1) return;
...
reportData(type, {msg: reason})
})
Ajax异常/fetch异常
if (window.XMLHttpRequest) {
const oldXmlHttpRequestSend = XMLHttpRequest.prototype.send;
const oldXmlHttpRequestOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, bool) {
// 拦截URL
// 记录请求开始时间 startTime,用于计算Ajax超时
}
XMLHttpRequest.prototype.send = funciton (data) {
// 记录状态码
if (status < 200 || status > 299) {
reportData(type, {...})
}
// 统计Ajax耗时 endTime - startTime
...
}
}
if ('fetch' in window) {
const oldFetch = window.fetch;
window.fetch = function() {
let args = Array.from(arguments);
return oldFetch.apply(this, args).then(res => {
...
})
}
}
浏览器奔溃异常
// 浏览器必须支持service worker,sw具有在浏览器奔溃的情况下依旧可以工作的特性
if (navigator.serviceWorker && window.addEventListener && window.postMessage) {
// 步骤
// 1.页面初始化时创建iframe标签
// 2.监听iframe的onload事件,初始化监听器
// 3.监听器中通过postMessage发送事件,心跳检测
// 4.sw监听postMessage事件,上报异常
var crashHost = 'https://aaa.bbb.cn';
var crashSrc = crashHost + '/front-end-look/crash';
var connect = function () {
var iframe = this;
var HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
const base_data = {}
// 记录上报类型
base_data['REPORT_TYPE'] = 'crash';
iframe.contentWindow.postMessage({
type: 'baseData',
id: '',
base_data: base_data,
report_url: 'xxxx'
}, crashSrc);
(function heartbeat() {
// 附加信息,如果页面 crash,上报的附加数据
var memoryData = {};
if (performance) {
var memory = performance.memory;
if (memory) {
memoryData.memory = {
totalJSHeapSize: memory.totalJSHeapSize,
usedJSHeapSize: memory.usedJSHeapSize,
jsHeapSizeLimit: memory.jsHeapSizeLimit,
usedRatio: (memory.usedJSHeapSize / memory.totalJSHeapSize * 100).toFixed(4) + "%"
}
}
}
var exp_data = {};
exp_data['_EXCEPTION._MESSAGE'] = JSON.stringify(memoryData);
// 记录上报时间
exp_data['_BASIC._CLI_TIME'] = new Date().getTime();
iframe.contentWindow.postMessage({
type: 'heartbeat',
id: '',
exp_data: exp_data,
}, crashSrc);
setTimeout(heartbeat, HEARTBEAT_INTERVAL);
})();
window.addEventListener('beforeunload', function () {
iframe.contentWindow.postMessage({
type: 'unload',
id: sessionId
}, crashSrc);
});
}
// 由于 sw 只能检测当前域名,所以这里检测当前的资源域名其实是没有意义的。
// 所以用 iframe 作中转。起到两个作用:
// 1、可以监听各个业务的崩溃异常,并通过真正的 crash 接口 去上报异常信息。
// 2、一定程度上,可以避免 sw 重复注册带来的性能损失。
var appendIframe = function () {
try {
if (document.body.querySelector('#crashIframe')) return;
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = crashSrc;
iframe.id = 'crashIframe';
iframe.addEventListener('load', connect, false);
document.body.appendChild(iframe);
} catch (e) {
console.log(e);
}
}
if (document.body) {
appendIframe();
} else {
win.addEventListener('load', appendIframe, false);
}
}
浏览器卡顿异常
// fps收集器
const fpsList = [];
function fpsCollect() {
// 收集1s之内的fps
}
window.requestAnimationFrame(fpsCollect);
四、异常日志缓存
如果要收集用户的行为日志,又要采取一定的技巧,而不能用户每一个操作后,就立即将该行为日志传到服务器,对于具有大量用户同时在线的应用,如果用户一操作就立即上传日志,无异于对日志服务器进行DDOS攻击。因此,我们先将这些日志存储在用户客户端本地,达到一定条件之后,再同时打包上传一组日志。
那么,如何进行前端日志存储呢?我们不可能直接将这些日志用一个变量保存起来,这样会挤爆内存,而且一旦用户进行刷新操作,这些日志就丢失了,因此,我们自然而然想到前端数据持久化方案。
目前,可用的持久化方案可选项也比较多了,主要有:Cookie、localStorage、sessionStorage、IndexedDB、webSQL 等等
通过对比,IndexedDB是最好的选择,它具有容量大、异步的优势,异步的特性保证它不会对界面的渲染产生阻塞。
五、异常上报时机、方式
1、上报时机
上报原则:所有的数据上报不能阻断或者影响用户的正常操作流程!
系统自动上报
-
发生错误即时上报
- 通过页面
beforeunload、onVisbilityChange、60s一次、创建<img />进行上报 unload时机触发,但要配合navigator.sendBeacon
- 通过页面
使用 sendBeacon() 方法会使用户代理(浏览器)在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。
用户手动上报
- 通过bug反馈入口进行上报反馈
2、上报方式
-
异步请求上报, 后端提供接口,或者直接发到日志服务器
-
img(gif)请求上报,URL参数带异常信息
(new Image()).src = 'http://yc.xxx.com/report?exp={xxx}'
六、异常告警和通知
对异常进行统计和分析只是基础,而在发现异常时可以推送和告警,甚至做到自动处理,才是一个异常监控系统应该具备的能力。
监控实现
当日志信息进入接入层时,就可以触发监控逻辑。当日志信息中存在较为高级别的异常时,也可以立即出发告警。
触发条件
除了系统开发时配置的默认告警条件,还应该提供给日志管理员可配置的自定义触发条件。
- 日志内含有什么内容时
- 日志统计达到什么度、量时
- 向符合什么条件的用户告警
推送渠道
可选择的途径有很多,例如邮件、短信、微信、电话。
推送频率
针对不同级别的告警,推送的频率也可以进行设定。低风险告警可以以报告的形式一天推送一次,高风险告警10分钟循环推送,直到处理人手动关闭告警开关。
自动报表
对于日志统计信息的推送,可以做到自动生成日报、周报、月报、年报并邮件发送给相关群组。
自动产生bug工单
当异常发生时,系统可以调用工单系统API实现自动生成bug单,工单关闭后反馈给监控系统,形成对异常处理的追踪信息进行记录,在报告中予以展示。
总结
本文介绍了关于前端异常板块常见的异常分类,并且给出了针对不同的异常采用不同的采集方式,为了避免上报数据太过于频繁可选择合适的日志进行缓存。
接着在一定的时机进行上报统计,而不恰当的上报方式可能会直接影响到正常的业务操作,选择的标准是无感知、不影响业务逻辑。
最后,需要及时响应或通知相关人员进行异常处理和反馈,形成监控闭环。