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打印