背景

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

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

模仿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本人上报,且会耗费肯定的内存

参考文档

  • 如何监控网页解体?
  • Service Worker and AJAX
  • 讲讲PWA