背景
web crash指的是页面的非正常卸载,此时不会触发页面的unload事件。
一般监控web crash就是利用没有unload事件这样一个特点:
在页面load后,往sessionStorage里面放一个tag: true, unload后置为false
方案一
window.addEventListener('load', function () {
sessionStorage.setItem('tag', 'true');
});
window.addEventListener('beforeunload', function () {
sessionStorage.setItem('tag', 'false');
});
if(sessionStorage.getItem('tag') &&
sessionStorage.getItem('tag') !== 'true') {
/** 页面异常退出了 */
}
思路:在页面load后,往sessionStorage里面放一个tag: true, unload后置为false。初始化时发现tag存在且为true,说明上一次是非正常卸载,上报crash
存在的问题:这种方案只适用于页面崩溃,并且用户在原浏览器tab重新打开崩溃页面的场景。用户打开tabA,tabA页面崩溃,用户强制关闭tabA或浏览器,此时的异常捕获不到
方案二
const currentPageId = Math.random() + '';
window.addEventListener('load', function () {
const pageObj = JSON.parse(localStorage.getItem('pageObj') || '""');
pageObj.currentPageId = 'true';
localStorage.setItem('pageObj', JSON.stringify(pageObj));
});
window.addEventListener('beforeunload', function () {
const pageObj = JSON.parse(localStorage.getItem('pageObj') || '""');
delete pageObj.currentPageId;
localStorage.setItem('pageObj', JSON.stringify(pageObj));
});
if(localStorage.getItem('pageObj')) {
// parse取出pageObj
for (let page in pageObj) {
if (page === 'true') {
/** 该页面异常退出了 */
delete pageObj[page];
}
}
}
思路:页面load时在localStroage中存储该页面的状态为true,页面卸载时移除。每次初始化页面时,遍历pageObj,发现存在page为true,说明该页面非正常卸载,上报crash
存在的问题:同一个页面,打开tabA,打开tabB,B页面检测到A页面的page为true,认为A页面crash并进行上报。但此时A页面正常运行
基于service-worker的web crash上报
什么是service-worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
通过chrome://inspect/#service-workers 可以查看浏览器注册的所有service-worker
service-worker是独立于页面的一个worker,页面JS线程挂掉后,不会影响service-worker工作。
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<button id="btn">click</button>
</body>
<script> document.getElementById('btn').addEventListener("click", () => {
console.log('clicked');
while(true) {}
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js', { scope: '/' }).then(function (registration) {
// Registration was successful
console.log('ServiceWorker registration successful with scope: ', '/');
}).catch(function (err) {
// registration failed :(
console.log('ServiceWorker registration failed: ', err);
});
if (navigator.serviceWorker.controller !== null) {
let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
let sessionId = Math.random() + '';
let heartbeat = function () {
console.log('heartbeat');
navigator.serviceWorker.controller.postMessage({
type: 'heartbeat',
id: sessionId,
data: {
key: 'some-data'
} // 附加信息,如果页面 crash,上报的附加数据
});
}
window.addEventListener("beforeunload", function () {
console.log('heartbeat');
navigator.serviceWorker.controller.postMessage({
type: 'unload',
id: sessionId
});
});
setInterval(heartbeat, HEARTBEAT_INTERVAL);
heartbeat();
}
}
</script>
</html>
// sw.js
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 检查一次
const CRASH_THRESHOLD = 15 * 1000; // 15s 超过15s没有心跳则认为已经 crash
const pages = {}
let timer;
function selfConsole(str) {
console.log('---sw.js:' + str) ;
}
function send(data) {
// @IMP: 此处不能使用XMLHttpRequest
// https://stackoverflow.com/questions/38393126/service-worker-and-ajax/38393563
fetch('/save-data', {
method: 'post',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(json)
.then(function (data) {
selfConsole('Request succeeded with JSON response', data);
})
.catch(function (error) {
selfConsole('Request failed', error);
});
}
function checkCrash(data) {
const now = Date.now()
for (var id in pages) {
let page = pages[id]
if ((now - page.t) > CRASH_THRESHOLD) {
// 上报 crash
delete pages[id]
send({
appName: data.key,
attributes: {
env: data.env || 'production',
pageUrl: location.href,
ua: navigator.userAgent,
msg: 'crashed',
content: '22222'
},
localDateTime: +new Date()
});
}
}
if (Object.keys(pages).length == 0) {
clearInterval(timer)
timer = null
}
}
self.addEventListener('message', (e) => {
const data = e.data;
if (data.type === 'heartbeat') {
pages[data.id] = {
t: Date.now()
}
selfConsole('recieved heartbeat')
selfConsole(JSON.stringify(pages));
if (!timer) {
timer = setInterval(function () {
selfConsole('checkcrash');
checkCrash(e.data.data)
}, CHECK_CRASH_INTERVAL)
}
} else if (data.type === 'unload') {
selfConsole('recieved unloaded')
delete pages[data.id]
}
})
代码上传到了 https://github.com/Lie8466/web-crash-report
模拟JS线程被block
打开localhost:5000,可以看到service注册成功,sw能够收到心跳且正常打印
点击click,页面JS线程进入死循环,不会再往sw发送心跳数据。等15s左右,sw定时器监听到该页面距离上次心跳超过15s,发送一个save请求
模拟页面挂掉
打开两个tab页,分别打开localhost:5000,都打开devTools。此时两个页面共用一个service-worker,console打印出两个页面的心跳数据
打开浏览器的任务管理器
可以看到service-worker有其单独的进程(是否是单独的进程取决于浏览器的分配情况,service-worker也可能是依附在某一个tab的进程中,像下图这种)
选中不与service worker共享进程的页面,终止进程。页面直接被终止,此时不会触发unload。触发了crash监控上报.
被终止的页面崩溃
另一个页面的network显示出save-data打印
注意
- Service Worker can only work on Https and localhost sites,非https页面navigator.serviceWorker是undefined,不能成功注册
- 由于sw权力比较大,可以代理页面内部的所有请求,所以其安全性要求特别高,注册的sw.js要求必须是页面域名下的,所以一般的错误监控SDK并不能使用service-worker监听业务方页面崩溃信息,需要业务方自己注册service-worker自己上报,且会消耗一定的内存
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。