监控系统之前端异常捕获

962 阅读5分钟

前言

监控系统在应用程序中收集和监视各种指标和数据,以便实时了解应用程序的性能、稳定性和用户体验。通过监控系统,开发团队可以及时发现和解决潜在的问题,提高应用程序的质量和用户满意度。

市面上有许多优秀的页面性能的监控工具如WebPagetest,PhantomJS,Page Speed等,但这些工具在测试中并不能反映性能的波动情况、业务相关的指标以及一些复杂而实时的数据采集。所以我们会选择在页面中植入 JS 来监控线上真实用户访问性能、行为体验,以及网站错误收集、可用性监控、日志监控等。

本文将从我最关心的错误异常监控开始进入监控系统的学习之旅

常见的几类错误捕获方法

全局错误检测监听 在 JavaScript 代码中,我们可以通过添加全局的错误事件监听器来捕获 JavaScript 错误,可以通过 window.onerror 或 window.addEventListener('error', handler) 来实现:

// 监听 js 错误
window.onerror = function(message, source, lineNo, colNo, error) { 
        // 将错误信息发送到监控系统
};
// 使用事件监听器,不但可以控制事件触发阶段,还可以捕获资源加载失败等问题。
window.addEventListener(
    'error', 
    function(event) {
        const target = event.target;
        if (target instanceof HTMLElement ){// && 相关加载错误类型判断
            // 从事件对象中获取错误信息,并发送到监控系统
         }
    },
    false
 );

未捕获异常监听 在 JavaScript 中,全局监听方式不能监听到所有类型的错误,如Promise相关的同步和异步代码错误,通过使用 window.addEventListener('unhandledrejection', handler) 来监听未捕获的 Promise 异常、请求异常等,并将其发送到监控系统。

addEventListener('unhandledrejection', (e) => { 
    monitor.errors.push({ 
        type: 'promise',
        msg: (e.reason && e.reason.msg) || e.reason || '', 
        // 错误发生的时间 
        time: new Date().getTime(),
        ... 
    });
});

捕获console.error 要捕获 console.error 的错误,可以通过重写 console.error 方法,下面是一种简单的实现的示例:

    // 重写 console.error
    console.error = function (...args) { 
        // 执行原始的 console.error 方法
        originalConsoleError.apply(console, args); 
        
        // 将错误信息发送到监控系统或自定义的错误处理函数 
        const errorData = { 
            message: args.join(' '), // 将错误信息转为字符串 
            stack: new Error().stack, // 获取错误的堆栈跟踪 
            timestamp: new Date().toISOString(), // 添加时间戳
        }; 
        
        // 发送错误数据到监控系统或自定义处理函数 
        sendErrorToMonitoringSystem(errorData); 
    };

一些系统事件的捕获 比如现代浏览器也有生命周期理念,当一个页面长时间未激活,再次打开会有重新刷新等操作。这背后就有一些浏览器主动冻结或废弃,而开发人员不能知道的过程。Chrome 68+ 中提供的freeze、resume事件,可以让我们监听 document 得知页面从 hidden 状态转变为冻结和非冻结状态。

document.addEventListener('freeze', (event) => {
    // The page is now frozen.
    // sendError 
 }); 
 document.addEventListener('resume', (event) => { 
     // The page has been unfrozen.
     // do something or sendError 
 });

页面崩溃检测 可以用load & beforeunload 配合service worker 做心跳检测。load事件是在页面加载后触发,beforeunload是正常网页关闭之前触发,而当网页崩溃时的关闭是无法触发beforeunload事件的,所以我们可以基于心跳检测的概念和这两个事件来实现网页崩溃的监控。

// 页面
// 注册service worker
navigator.serviceWorker.register('./sw.js').then(registration => {
  // 发送注册message
  tryToRegister();
})

// 接收心跳包,并回发
navigator.serviceWorker.addEventListener('message', function(event){
  if (event.data.type === 'checkHealth') {
    sendMessage({type: 'keepHealth'});
  }
});
// 页面关之前发送退出信息
window.addEventListener("beforeunload", function (event) {
  sendMessage({
    type: 'unregister',
  })
});
// sw 
const heartDetection = {};
function checkHealth(id) {
  if (heartDetection[id]) {
    // 状态不健康就上报
    if (heartDetection[id].flag !== 'healthy') {
      // do something
      // reportCrash(heartDetection[id].reportData);
      removeCheck(id);
      return;
    }
    // 设置成不健康,下次定时器的时候检查
    heartDetection[id].flag = 'unhealthy';
    sendMessageToClient(heartDetection[id].client, {type: 'checkHealth'})
  }
}

self.addEventListener('message', function(event){
  const sourceId = event.source.id;
  switch (event.data.type) {
    // 页面新来的时候注册
    case 'register':
      // 根据id拿到对应的页面
      self.clients.get(sourceId)
        .then(function(client) {
          // 把id 和 client存储起来
          // 执行循环检测 & 发送心跳包
          heartDetection[sourceId] = {
              client,
              flag: healthy,
              timer: setInterval(function() {
                  checkHealth(sourceId);
                }, 5000),
              ...
          }
        })
        .catch(function(err) {
          console.log(err);
        })
      break;
    // 页面关闭的时候删除有关信息
    case 'unregister':
      // 清理心跳包, clearInterval
      ....
      break;
    case 'keepHealth':
      // 如果收到健康标识的更新状态
      if(heartDetection[sourceId]) {
        heartDetection[sourceId].flag = 'healthy';
      }
  }
});

页面主动上报业务错误 提供上报方法为特殊的业务逻辑或是自定义错误记录,提供给网站完善报警链路的自定义能力。

其他

以上粗略的描述了几种错误捕获的方式,而实际监控系统的实现更为复杂,面临的困难更多,常见的问题如:

  • 前端应用程序中的错误类型多种多样,包括 JavaScript 错误、网络请求错误、资源加载错误等。这些错误可能有不同的根本原因和堆栈跟踪,导致错误聚合时需要处理不同类型和结构的数据。

  • 同时大型应用程序可能会产生大量的错误,这包括重复的错误、崩溃的错误和较小的错误。

  • 除了错误类型多,错误量大,错误还有关联性,一个错误可能会引发一系列相关的错误。例如,一个 JavaScript 错误可能导致网络请求失败和其他后续错误,正确地将这些相关错误聚合在一起,以便能够全面地理解错误链的整体情况,也是一个挑战。

  • 而错误聚合需要处理错误的堆栈跟踪信息,对其可能包含大量信息和嵌套,需要进行解析和处理,以便能够提取关键信息并进行错误聚合,同时需要考虑错误发生的环境和上下文信息,如浏览器类型、操作系统、用户设备等。

对于以上问题也有一些解决策略,如将相似的错误进行归类和去重,减少重复的错误报告提高错误聚合的效率和可读性;使用唯一标识符或错误链路追踪技术,将相关的错误关联起来,以便能够追踪错误发生的路径和影响范围。

当然监控系统的能力不仅仅包含错误监控,性能监控,用户行为跟踪以及这些信息排队发送的机制也是核心能力,道路漫漫持续学习吧。


参考文档

7 天打造前端性能监控系统 - FEX (baidu.com)

前端监控原理深入剖析 (freecodecamp.org)

监控网页崩溃的方案思考 - CodeLife - Ysom

EventTarget.addEventListener() - Web API 接口参考 | MDN (mozilla.org)