背景
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') { /** 页面异样退出了 */ }
然而这种计划只实用于页面解体,并且用户在原浏览器tab从新关上解体页面的场景。更实用的办法是应用localStorage
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(sessionStorage.getItem('pageObj')) { // parse取出pageObj for (let page in pageObj) { if (page === 'true') { /** 该页面异样退出了 */ delete pageObj[page]; } } }
这种用法的缺点是,如果关上tabA页面没有敞开,又关上tabB,会认为A页面是异样退出,触发crash上报。
所以不论是基于sessionStorage还是localStorage,都有缺点,而且依赖用户再次关上页面的行为。最好的计划是基于service-worker来写
基于service-worker的web crash上报
什么是service-worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
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.jsconst CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 查看一次const CRASH_THRESHOLD = 15 * 1000; // 15s 超过15s没有心跳则认为曾经 crashconst 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](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本人上报,且会耗费肯定的浏览器内存### 参考文档> [https://zhuanlan.zhihu.com/p/40273861](https://zhuanlan.zhihu.com/p/40273861)