web crash監控

背景

web crash指的是頁面的非正常卸載,此時不會觸發頁面的unload事件。
image.pnghtml

通常監控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的web crash上報

什麼是service-worker: https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API

service-worker是獨立於頁面的一個worker,頁面JS線程掛掉後,不會影響service-worker工做。segmentfault

image

<!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可以收到心跳且正常打印
image.png瀏覽器

點擊click,頁面JS線程進入死循環,不會再往sw發送心跳數據。等15s左右,sw定時器監聽到該頁面距離上次心跳超過15s,發送一個save請求安全

image.png

模擬頁面掛掉

打開兩個tab頁,分別打開localhost:5000,都打開devTools。此時兩個頁面共用一個service-worker,console打印出兩個頁面的心跳數據session

image.png

打開瀏覽器的任務管理器

image.png
image.png

能夠看到service-worker有其單獨的進程(是不是單獨的進程取決於瀏覽器的分配狀況,service-worker也多是依附在某一個tab的進程中,像下圖這種)
image.png

選中不與service worker共享進程的頁面,終止進程。頁面直接被終止,此時不會觸發unload。觸發了crash監控上報.

被終止的頁面崩潰
image.png
另外一個頁面的network顯示出save-data打印
image.png

注意

  • Service Worker can only work on Https and localhost sites,非https頁面navigator.serviceWorker是undefined,不能成功註冊
  • 因爲sw權力比較大,能夠代理頁面內部的全部請求,因此其安全性要求特別高,註冊的sw.js要求必須是頁面域名下的,因此通常的錯誤監控SDK並不能使用service-worker監聽業務方頁面崩潰信息,須要業務方本身註冊service-worker本身上報,且會消耗必定的內存

參考文檔

相關文章
相關標籤/搜索