捕捉异常 🔍 前端页面崩溃监控技术方案

886 阅读9分钟

3.png

一、前言

image001.png

前端页面的稳定性在用户体验中扮演着较为重要的角色,稳定性问题不仅可能会导致用户流失,还可能对品牌形象和信任度产生深远的负面影响。现在,我们已经对页面白屏和卡顿等稳定性问题给予了足够的重视,但是对于偶发的页面崩溃问题却关注不足。然而,从影响程度来看,页面崩溃无疑比页面卡顿更加严重。页面卡顿可能只是由于 JS 执行响应缓慢导致一些内容无法及时执行,经过一段时间等待后页面可能恢复正常,但是页面崩溃不同,页面崩溃意味着整个页面彻底失效,不在后台继续运行,即使等待一段时间也不会恢复,页面崩溃后我们的代码变得无效,监控上报也无法进行。随着开发技术的迅猛发展,前端应用的复杂性不断增加,页面崩溃的问题也日益突出,有效地监控和预防页面崩溃成为了前端开发面临的难题之一。本文将主要探讨页面崩溃的三种监控策略,通过“本地状态存储对比、Service Worker 心跳检测、Reporting API 上报”等技术方案捕获崩溃数据,帮助开发者定位异常并分析崩溃原因,从而迅速响应减轻用户受到的影响,提升应用的可靠性和用户满意度。

二、技术方案

2.1 本地状态存储对比

页面崩溃后,主线程便停止了工作,这意味着我们无法在当前页面执行的主线程中进行任何监控或上报操作。因此,我们需要转变下思路。通常情况下,当用户的页面发生崩溃时,他们会尝试刷新页面。既然当前的执行流程无法完成上报,我们可以利用 IndexDB 或 localStorage 等进行本地状态存储,当用户下次打开页面时,主线程可以读取这些存储的数据,然后进行对比判断是否发生崩溃并进行上报。

window.addEventListener('load', function () {
  sessionStorage.setItem('good_exit', 'pending');
  setInterval(function () {
    sessionStorage.setItem('time_before_crash', new Date().toString());
  }, 1000);
});

window.addEventListener('beforeunload', function () {
  sessionStorage.setItem('good_exit', 'true');
});

if(sessionStorage.getItem('good_exit') && sessionStorage.getItem('good_exit') !== 'true') {
  alert('Hey, welcome back from your crash, looks like you crashed on: ' + sessionStorage.getItem('time_before_crash'));
}

image003.png

  1. 第一次进入页面,存储 good_exit 为 pending,并且每隔 1s 更新有效时间;
  2. 页面正常关闭,beforeunload 监听事件将 good_exit 设置为 true;
  3. 第二次进入页面,判断 good_exit 为 true,则上一次正常关闭,继续将值设置为 pending;
  4. 页面发生崩溃,不会触发 beforeunload 监听事件将 good_exit 设置为 true;
  5. 第三次进入页面,判断 good_exit 存在且为 pending,则认为上次是异常关闭,上报信息;

除了单个状态的记录之外,也可以使用记录心跳的方法来进行信息比对,例如每隔 20s 更新 localStotage 中存储的 time,页面 unload 时移除记录,页面初始化时检测时间,如果时间信息存在且超过 10s 则判定为发生异常关闭。

const UPDATE_INTERVAL = 20 * 1000;
const CRASH_THRESHOLD = 10 * 1000; 
const monitorId = 'crashMonitor';
window.addEventListener('load', function () {
    setInterval(() => {
        localStorage.setItem(monitorId, +new Date());
    }, UPDATE_INTERVAL);
});
window.addEventListener('beforeunload', function () {
    localStorage.removeItem(monitorId);
});
if (localStorage.getItem(monitorId) && +new Date() - localStorage.getItem(monitorId) > CRASH_THRESHOLD) {
    // ... 上报 crash
}

本地状态存储对比实现简单但有着非常多明显的缺点。首先,依赖用户操作,这是不可控的,只有再次进入页面才能进行判断并上报;并且,本地存储方式的选择也有着缺陷,sessionStorage 存储仅适用于在浏览器原 tab 下重新打开场景,用户关闭 tabA 或浏览器再打开页面就无法捕获,localStorage 存储仅适用于只打开一个页面的场景,用户 tabA 打开页面后再在 tabB 打开页面,此时会误认为 tabA 已崩溃未正常退出;信息上报的时效性也不可控,如果用户页面崩溃后直接离开浏览器不再执行操作,且间隔较长时间才再来进入页面则上报的时效性就大打折扣了。

2.2 Service Worker 心跳检测

页面崩溃后,主线程便停止了工作,我们再次转变下思路,既然主线程无法工作,是否有什么可以脱离主线程执行代码的呢?答案就是 Service Worker。Service Worker 在浏览器中是有自己独立的线程的,浏览器崩溃后,Service Worker 所在的线程是不受影响的,并且它与主线程可以通过 navigator.serviceWorker.controller.postMessage API 进行通信。注意,在崩溃场景中我们不使用 Web Worker 来进行心跳检测,因为Web Worker 是由页面创建并管理的,它的生命周期与页面绑定。如果页面崩溃或被关闭,Web Worker 也会被销毁。Service Worker 是独立于页面的,它的生命周期由浏览器管理,而不是依赖于某个特定的页面。

/**
 * 主线程
 */
navigator.serviceWorker?.register?.("/sw.js").then(() => {
  console.log("ServiceWorker registration success!");
});
if (navigator.serviceWorker?.controller !== null) {
  let HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
  let sessionId = Math.random();
  let heartbeat = function () {
    navigator.serviceWorker.controller.postMessage({
      type: "heartbeat",
      id: sessionId,
      data: {}, // 附加信息,如果页面 crash,上报的附加数据
    });
  };
  window.addEventListener("beforeunload", function () {
    navigator.serviceWorker.controller.postMessage({
      type: "unload",
      id: sessionId,
    });
  });
  setInterval(heartbeat, HEARTBEAT_INTERVAL);
  heartbeat();
}
/**
 * Service Worker
 */
const CHECK_CRASH_INTERVAL = 10 * 1000; // 每 10s 检查一次
const CRASH_THRESHOLD = 15 * 1000; // 超过 15s 没有心跳则认为已经 crash
const pages = {};

let timer;

function checkCrash() {
  const now = Date.now();
  for (var id in pages) {
    let page = pages[id];
    console.log("check", page.t, now - page.t);
    if (now - page.t > CRASH_THRESHOLD) {
      // ... 上报 crash
      console.log("crash");
      delete pages[id];
    }
  }
  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(),
    };
    console.log("heartbeat", pages[data.id], pages[data.id].t);
    if (!timer) {
      timer = setInterval(function () {
        checkCrash();
      }, CHECK_CRASH_INTERVAL);
    }
  } else if (data.type === "unload") {
    console.log("unload");
    delete pages[data.id];
  }
});
  1. 主线程:通过 postMessage 每 5s 给 sw 发送一个心跳,表示在线,sw 更新登记时间;
  2. 主线程:网页 beforeunload 时通过 postMessage 告知 sw 页面已正常关闭并清除信息;
  3. 主线程:网页崩溃停止调用 postMessage,sw 登记时间停留在奔溃前的最后一次心跳;
  4. SW:Service Worker 每 10s 查看一遍网页更新时间,发现时间超出阈值则判定崩溃了;

Service Worker 心跳检测同样也有着一些缺点,崩溃时间的检测精度同样是根据用户设置什么样的间隔时间进行有效时间判断,使用时需要衡量时间精度和执行开销之间的平衡点,同时 Service Worker 具有较高的安全想只能在支持 HTTPS 的网站以及本地开发环境中的 localhost 上运行。此外,如果时间间隔设置不合理可能会将页面卡顿误检为页面崩溃,但是心跳检测比较适合于想自定义一些信息进行上报的场景,可以将页面运行一些辅助排查信息传给 SW,在崩溃时进行上报。

2.3 Reporting API 上报

Reporting API 引入了一个 HTTP Header Reporting-Endpoints,其允许开发人员以自定义的方式来将浏览器的报告发送到指定服务器。通过该方式我们能够捕获和报告网站中可能发生的各种错误,包括:

注意,Report-To 已弃用,请使用 Reporting-Endpoints!

Reporting-Endpoints: my-endpoint="https://example.com/reports
POST /reports HTTP/1.1
Host: example.com
...
Content-Type: application/reports+json

[{
  "type": "crash",
  "age": 42,
  "url": "https://example.com/",
  "user_agent": "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0",
  "body": {
    "reason": "oom"
  }
}]

使用该功能需要自行建立一个服务来接收这些报告,同时这是一个实验性 API,存在一定的兼容性问题,更多内容请阅读 Monitor your web application with the Reporting APICrash Reporting。Reporting API 对于前端监控来说无疑是一个巨大的福音,它极大地简化了前端监控的复杂性,减少了我们原本需要手动完成的工作量,随着浏览器技术的不断进步,预计会有更多类型的上报数据被纳入 Reporting API 的支持范围。

三、模拟崩溃

在浏览器中,可以通过一些极端操作来模拟页面崩溃。以下是几种常见的方法:

  1. 通过无限递归调用函数,导致调用栈溢出,从而引发页面崩溃。
function crash() {
  crash(); // 无限递归
}
crash();
  1. 通过不断创建无法被垃圾回收的对象,内存泄漏,导致页面崩溃。
let arr = [];
while (true) {
  arr.push(new Array(1000000).fill(0)); // 不断分配大数组
}
  1. Chrome 浏览器中输入chrome://crash/模拟崩溃。

四、总结

方案优点缺点
本地状态存储对比实现简单且成本较低数据不准确且不可控,依赖用户行为,信息上报可能不及时
Service Worker 心跳检测实现简单且相对准确崩溃上报信息由开发者自行定义,且 Service Worker 对安全性要求较高
Reporting API 上报上报时机相对准确开发成本高需要服务端来接收报告,且不支持额外自定义信息上报

本文主要介绍了“本地状态存储对比、Service Worker 心跳检测、Reporting API 上报”等三种前端页面崩溃监控技术方案。这些方案各自具有独特的优势和局限性,适用于不同的应用场景和需求,可以根据实际情况灵活选择和整合这些技术方案,以实现最佳的页面崩溃监控效果。

参考资料

「前端监控专栏」更多内容 👇