浅析前端异常与性能监测
1. 接口加载时间和是否成功监控
// fetch
function FetchReset () {
if(!window.fetch) return;
let _oldFetch = window.fetch;
window.fetch = function () {
return _oldFetch.apply(this, arguments)
.then(res => {
if (!res.ok) {
// 调用失败,上报错误
return Promise.reject(res)
}
return res;
}, (err) => {
// 错误执行函数,上报错误
return Promise.reject(res)
})
// 当fetch方法错误时上报
.catch(error => {
// error.message,
// error.stack
// 抛出错误并且上报
throw new Error(JSON.stringify(error));
})
}
}
//XMLHttpRequest
function AjaxReset () {
let protocol = window.location.protocol;
if (protocol === 'file:') return;
// 处理XMLHttpRequest
if (!window.XMLHttpRequest) {
return;
}
let xmlhttp = window.XMLHttpRequest;
// 保存原生send方法
let _oldSend = xmlhttp.prototype.send;
let _handleEvent = function (event) {
try {
if (event && event.currentTarget && event.currentTarget.status !== 200) {
// event.currentTarget 即为构建的xhr实例
// event.currentTarget.response
// event.currentTarget.responseURL || event.currentTarget.ajaxUrl
// event.currentTarget.status
// event.currentTarget.statusText
});
}
} catch (e) {va
console.log('Tool\'s error: ' + e);
}
}
xmlhttp.prototype.send = function () {
this.addEventListener('error', _handleEvent); // 失败
this.addEventListener('load', _handleEvent); // 完成
this.addEventListener('abort', _handleEvent); // 取消
return _oldSend.apply(this, arguments);
}
}
2. 页面性能(dns解析等)
白屏时间和首屏时间有争议,可以再进一步查阅书籍和文档确定,这里按我认为正确的版本讲述
先看看以下几个重要指标,或许对上面的几个统计,就会明朗许多:
fetchStart:浏览器准备好使用http请求抓取文档的时间(发生在检查本地缓存之前)。
domainLookupStart:DNS域名查询开始的时间,如果使用了本地缓存话,或 持久链接,该值则与fetchStart值相同。
domainLookupEnd:DNS域名查询完成的时间,如果使用了本地缓存话,或 持久链接,该值则与fetchStart值相同。
- connectStart:HTTP 开始建立连接的时间,如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接开始时间。
- secureConnectionStart:HTTPS 连接开始的时间,如果不是安全连接,则值为 0
- connectEnd:HTTP完成建立连接的时间(完成握手)。如果是持久链接的话,该值则和fetchStart值相同,如果在传输层发生了错误且需要重新建立连接的话,那么在这里显示的是新建立的链接完成时间。
- requestStart:http请求读取真实文档开始的时间,包括从本地读取缓存,链接错误重连时。
- responseStart:开始接收到响应的时间(获取到第一个字节的那个时候)。包括从本地读取缓存。
- responseEnd:HTTP响应全部接收完成时的时间(获取到最后一个字节)。包括从本地读取缓存。
- domLoading:开始解析DOM树的时间。
- domInteractive:完成解析DOM树的时间(只是DOM树解析完成,但是并没有开始加载网页的资源)。
- domContentLoadedEventStart:DOM解析完成后,网页内资源加载开始的时间。
- domContentLoadedEventEnd:DOM解析完成后,网页内资源加载完成的时间。(异步脚本加载完没有执行)
- domComplete:DOM树解析完成,且资源也准备就绪的时间。Document.readyState 变为 complete,并将抛出 readystatechange 相关事件。 (异步脚本加载完成,也执行完成)
- loadEventStart:load事件发送给文档。也即load回调函数开始执行的时间,如果没有绑定load事件,则该值为0.(所有文件都已经加载完成 css js img等)
- loadEventEnd:load事件的回调函数执行完毕的时间,如果没有绑定load事件,该值为0.
- 白屏时间 从我们打开网站到有内容渲染出来的时间点。domContentLoadedEventStart - fetchStart
- 首屏时间 首屏内容渲染完毕的时间节点。 domComplete - fetchStart
- DNS 解析 domainLookupEnd - domainLookupStart
- TCP 连接 connectEnd - connectStart
- 页面总下载时间 responseEnd - requestStart
- DOM 解析 用户可操作(dom ready)时间节点。 domInteractive - domLoading
- 资源加载时间 window.onload的触发节点。 loadEventStart - domContentLoadedEventStart
3. 页面报错监控
1. try{} catch(e) {} 无法捕获异步异常
2. window.onerror 无法捕获静态资源获取异常
3. window.addEventListener('error', event => {
// 过滤js error
let target = event.target || event.srcElement;
let isElementTarget = target instanceof HTMLScriptElement || target instanceof HTMLLinkElement || target instanceof HTMLImageElement;
if (!isElementTarget) return false;
// 上报静态资源异常
// 无法捕获接口异常,比如404, 但是图片和js css加载失败能监测
})
4. promise 捕获异常
window.addEventListener("unhandledrejection", function(e){
// e.preventDefault(); // 阻止异常向上抛出
console.log('捕获到异常:', e);
});
Promise.reject('promise error');
5. 网页崩溃异常(用户在线时长)
随着 PWA 概念的流行,大家对 Service Worker 也逐渐熟悉起来。基于以下原因,我们可以使用 Service Worker 来实现网页崩溃的监控:
Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态;
网页可以通过 navigator.serviceWorker.controller.postMessage API 向掌管自己的 SW 发送消息。
基于以上几点,我们可以实现一种基于心跳检测的监控方案:
p1:网页加载后,通过 postMessage API 每 5s 给 sw 发送一个心跳,表示自己的在线,sw 将在线的网页登记下来,更新登记时间;
p2:网页在 beforeunload 时,通过 postMessage API 告知自己已经正常关闭,sw 将登记的网页清除;
p3:如果网页在运行的过程中 crash 了,sw 中的 running 状态将不会被清除,更新时间停留在奔溃前的最后一次心跳;
sw:Service Worker 每 10s 查看一遍登记中的网页,发现登记时间已经超出了一定时间(比如 15s)即可判定该网页 crash 了。
一些简化后的检测代码,给大家作为参考:
// 页面 JavaScript 代码
if (navigator.serviceWorker) {
navigator.serviceWorker.register('sw.js', {scope: '/a/b/c/'})
.then(function (reg) {
sendStart()
//console.log(reg.scope);
// scope => https://yourhost/a/b/c/
});
}
sendStart = () => {
if (navigator.serviceWorker.controller !== null) {
let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
let sessionId = uuid();
let heartbeat = function () {
navigator.serviceWorker.controller.postMessage({
type: 'heartbeat',
id: sessionId,
data: {} // 附加信息,如果页面 crash,上报的附加数据
});
}
window.addEventListener("beforeunload", function() {
navigator.serviceWorker.controller.postMessage({
type: 'unload',
id: sessionId
});
});
setInterval(heartbeat, HEARTBEAT_INTERVAL);
heartbeat();
}
}
sessionId 本次页面会话的唯一 id;
postMessage 附带一些信息,用于上报 crash 需要的数据,比如当前页面的地址等等。
// sw.js
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 检查一次
const CRASH_THRESHOLD = 15 * 1000; // 15s 超过15s没有心跳则认为已经 crash
const pages = {}
let timer
function checkCrash() {
const now = Date.now()
for (var id in pages) {
let page = pages[id]
if ((now - page.t) > CRASH_THRESHOLD) {
// 上报 页面崩溃
delete pages[id]
}
}
if (Object.keys(pages).length == 0) {
clearInterval(timer)
timer = null
}
}
worker.addEventListener('message', (e) => {
const data = e.data;
if (data.type === 'heartbeat') {
pages[data.id] = {
t: Date.now()
}
if (!timer) {
timer = setInterval(function () {
checkCrash()
}, CHECK_CRASH_INTERVAL)
}
} else if (data.type === 'unload') {
delete pages[data.id]
}
})
6. 页面未崩溃时统计在线时长
使用 sendBeacon() 方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。
window.addEventListener('unload', logData, false);
function logData() {
navigator.sendBeacon("/log", analyticsData);
}
5. 框架报错
react 异常捕获
class ErrorCatch extends React.Component {
constructor(props) {
super(props);
}
componentDidCatch(error, info) {
// 上报错误
}
render() {
this.props.children;
}
}
6. 设备信息获取(浏览器、设备、系统)
- 获取ip、城市等信息(这个一般是服务端获取,直接统计在数据库)
console.log(
"IP: " + returnCitySN['cip'] +
"地区代码: " + returnCitySN['cid'] +
"所在地: " + returnCitySN['cname']
)
- 获取操作系统分布、浏览器分布、设备分布
// 系统平台
navigator.platform MacIntel win32
// 浏览器和系统信息
navigator.userAgent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36"
// 获取设备信息
let MobileDetect = require('mobile-detect')
let md = new MobileDetect(req.headers['user-agent']);
// 或者
var md = new MobileDetect(
'Mozilla/5.0 (Linux; U; Android 4.0.3; en-in; SonyEricssonMT11i' +
' Build/4.1.A.0.562) AppleWebKit/534.30 (KHTML, like Gecko)' +
' Version/4.0 Mobile Safari/534.30');
console.log( md.mobile() ); // 'Sony'
console.log( md.userAgent() ); // 'Safari'
console.log( md.os() ); // 'AndroidOS'
console.log( md.is('iPhone') ); // false
console.log( md.versionStr('Build') ); // '4.1.A.0.562'