web crash指的是页面的非正常卸载,此时不会触发页面的unload事件。html
通常监控web crash就是利用没有unload事件这样一个特色:
在页面load后,往sessionStorage里面放一个tag: true, unload后置为falsegit
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,说明上一次是非正常卸载,上报crashgithub
存在的问题:这种方案只适用于页面崩溃,而且用户在原浏览器tab从新打开崩溃页面的场景。用户打开tabA,tabA页面崩溃,用户强制关闭tabA或浏览器,此时的异常捕获不到web
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,说明该页面非正常卸载,上报crashajax
存在的问题:同一个页面,打开tabA,打开tabB,B页面检测到A页面的page为true,认为A页面crash并进行上报。但此时A页面正常运行json
什么是service-worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
service-worker是独立于页面的一个worker,页面JS线程挂掉后,不会影响service-worker工做。segmentfault
<!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
打开localhost:5000,能够看到service注册成功,sw可以收到心跳且正常打印浏览器
点击click,页面JS线程进入死循环,不会再往sw发送心跳数据。等15s左右,sw定时器监听到该页面距离上次心跳超过15s,发送一个save请求安全
打开两个tab页,分别打开localhost:5000,都打开devTools。此时两个页面共用一个service-worker,console打印出两个页面的心跳数据session
打开浏览器的任务管理器
能够看到service-worker有其单独的进程(是不是单独的进程取决于浏览器的分配状况,service-worker也多是依附在某一个tab的进程中,像下图这种)
选中不与service worker共享进程的页面,终止进程。页面直接被终止,此时不会触发unload。触发了crash监控上报.
被终止的页面崩溃
另外一个页面的network显示出save-data打印