性能文章>从零开始搞监控系统(7)——监控页面奔溃>

从零开始搞监控系统(7)——监控页面奔溃原创

1年前
339134

目录:

从零开始搞监控系统(1)——SDK

从零开始搞监控系统(2)——存储和分析

从零开始搞监控系统(3)——性能监控

从零开始搞监控系统(4)——内存泄漏

从零开始搞监控系统(5)——小程序监控

前言

页面奔溃包含两种场景,第一种是浏览器在加载网页时遇到问题导致的奔溃,另一种是因为脚本渲染出错导致页面空白无内容的奔溃。

前段时间运营抱怨有张活动页出现了空白(第二种奔溃场景),导致用户无法访问,希望我们能主动监控到这种情况,而不是通过用户的上报。

后面和运维沟通,他那边目前只能监控接口的访问情况,无法监控静态资源,若要监控得自己想办法实现。

首先想到的自然是利用现有的监控系统来了解页面空白情况,例如某个项目5分钟内没有监控日志,那就认为出现了页面奔溃。

急匆匆的写了段定时任务,放到线上运行,发现这样监控会有一个很大漏洞。因为某些项目的访问量本来就不高,5分钟内没有日志是属于正常情况,所以只得作罢。

一、页面奔溃

  首先来解决第一种奔溃场景,在网上搜了些关键字,发现了些有用的资料,例如如何监控网页崩溃前端崩溃监控优化历程等。

  这些资料提供了一个全新的思路来监控页面奔溃,基于Service Worker的崩溃统计方案。

  简单地说就是一种心跳检测机制,在页面的脚本中创建Service Worker工作线程,然后定时地向该线程发送消息,即使网页奔溃了,线程还能存活。

  在线程中接收消息并比对时间,当间隔时间大于15秒时,就认为超时没有心跳了,页面处于奔溃阶段,向监控系统上报相关信息。

  在我操刀实现的时候,Service Worker没有运行成功,后面就改成了Web Worker

  工作线程的代码保存在sw.js(如下所示),在参考一篇Web Workers的文章时,他提到在线程中可以navigator对象,该对象正好有个sendBeacon()方法,可用于跨域请求。

  但是没想到线程中用的WorkerNavigator,并没有该方法,后面无奈改成了fetch()

  但是有跨域问题,要么在响应时加上跨域头,要么就无视直接发送,因为浏览器只会拦截响应不会拦截请求。

var CHECK_CRASH_INTERVAL = 10 * 1000;     // 每 10s 检查一次
var CRASH_THRESHOLD = 15 * 1000;          // 15s 超过15s没有心跳则认为已经 crash
var pages = {}, timer;
function send(param) {
    fetch(param.src);
};
function checkCrash() {
  var now = Date.now()
  for (var id in pages) {
    var page = pages[id];
    if ((now - page.t) > CRASH_THRESHOLD) {
      // 上报 crash
      delete pages[id];
      send(page.data);
    }
  }
  if (Object.keys(pages).length == 0) {
    clearInterval(timer)
    timer = null
  }
}
self.addEventListener('message', (e) => {
  var data = e.data;
  if (data.type === 'heartbeat') {
    // console.log('heartbeat');
    pages[data.id] = {
      t: Date.now(),
      data: data.data
    }
    if (!timer) {
      timer = setInterval(function () {
        checkCrash()
      }, CHECK_CRASH_INTERVAL)
    }
  } else if (data.type === 'unload') {
    delete pages[data.id]
  }
})

  在网页中加的代码如下,由于Worker加载的脚本有同源策略的限制,所以脚本和页面需要在相同的域名中。

function monitorCrash(param) {
  var isCrash = param.isCrash;
  if (!isCrash || !window.Worker) return;
  var worker = new Worker("/sw.js");
  var HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
  var sessionId = getIdentity();
  var heartbeat = function () {
      worker.postMessage({
        type: "heartbeat",
        id: sessionId,
        data: {
          //在页面奔溃时,上报数据,需要将上报地址一起传递
          src: param.src
        }
      });
  };
  window.addEventListener("beforeunload", function () {
    worker.postMessage({
      type: "unload",
      id: sessionId
    });
  });
  var timer = setInterval(heartbeat, HEARTBEAT_INTERVAL);
  heartbeat();
}

  上线后先在管理后台做测试,管理后台使用的是PC浏览器,马上就发现了比较严重的误报问题。

  分析下来有可能是网页在标签栏不活动的时候,影响了定时器的执行,再次活动计算两个时间段的间隔,很有可能超出了15秒,而上报奔溃日志。

  鉴于此,在没有完美解决方案之前,暂时将此功能下架。

二、页面空白

  再来解决第二种奔溃场景,现在开发都会依托React或Vue等库或框架,而这些都是用脚本来渲染出DOM结构的。

  一旦在渲染时出现脚本错误(例如未定义的变量、浏览器不支持的语法等)就会中断渲染,从而就会出现页面无内容的情况。

  这类监控并不需要使用Web Worker,只要我的监控SDK在业务脚本之前引入,就能保证监控代码正常运行。

  监控原理就是加个定时器,查看渲染容器中是否是空白,若是空白就上报并关闭定时器,否则循环监控。

  例如后台管理系统采用的是React,在HTML中会声明一个div元素,内容都会渲染到该元素中。

<div id="root"></div>

  自定义一个关键DOM的判断条件,如下所示,在定时器中循环执行。

shin.setParam({
  validateCrash: () => {
    //当root标签中的内容为空时,可认为页面已奔溃
    return {
      success: document.getElementById("root").innerHTML.length > 0,
      prompt: "页面出现空白"
    };
  }
});

  此处还有个小坑,就是定时器的运行时机,不能太早,太早判断的话,div元素中肯定没有内容,后面就将判断时机移到了DOMContentLoaded事件中。

  下面是监控白屏的主要逻辑,isCrash 是一个监控开关,document.body.clientHeight 是指内容高度与上下内边距的和。

  在我们这边的页面中, body 都不会有内边距,所以该判断适用,当然,具体可根据业务场景做自定义的兜底处理。

function monitorCrash(param) {
  var isCrash = param.isCrash;
  var validateCrash = param.validateCrash;
  if (!isCrash) {
    return;
  }
  var HEARTBEAT_INTERVAL = 5 * 1000; // 每五秒发一次心跳
  var crashHeartbeat = function () {
    var result = validateCrash();
    // 符合自定义的奔溃规则
    if (result && !result.success) {
      handleError({
        type: ERROR_CRASH,
        desc: {
          prompt: result.prompt,
          url: location.href
        }
      });
      // 关闭定时器
      clearInterval(timer);
      // 兜底白屏算法,可根据自身业务定义
    } else if (document.body.clientHeight === 0) {
      // 查询第一个div
      var currentDiv = document.querySelector("div");
      // 增加 html 字段是为了验证是否出现了误报
      handleError({
        type: ERROR_CRASH,
        desc: {
          prompt: "页面没有高度",
          url: location.href,
          html: currentDiv ? currentDiv.innerHTML : ""
        }
      });
      clearInterval(timer);
    }
  };
  var timer = setInterval(crashHeartbeat, HEARTBEAT_INTERVAL);
  crashHeartbeat(); // 立即执行一次
  // 5分钟后自动取消定时器
  setTimeout(function () {
    // 关闭定时器
    clearInterval(timer);
  }, 1000 * 300);
}

  2022-12-07一开始 isCrash 默认标记为 false,也就是关闭监控的,后面默认打开后,线上出现白屏的页面一下子增加了四五百左右。

  接下来就是验证上报的白屏是否准确,下面是上报的一条记录,它有一串字符身份信息,例如 syqgpsyz4s。

{
  "type": "crash",
  "desc": {
    "prompt": "页面没有高度",
    "url": "https://www.xxx.com/chat.html?matchId=100",
    "html": ""
  }
}

  根据身份信息,再去日志明细中查找他的前后动作,发现只有一条记录,也就是既没有脚本错误,也没有接口请求。

  

  再根据此身份去查询性能监控的记录 ID,找出当时静态资源的瀑布图,在此图中,并没有发现资源异常。

  

  但是当我直接请求 url 地址时,却发现有 3 个资源的请求是 404,与正常页面中的 3 个资源做比对,发现两者的随机后缀是不同的。

  

  现在恍然大悟,是 CDN 缓存刷新失败导致的问题,问题马上就定位到了。

  还发现另一个问题是因为参数的值导致的白屏,首次使用下来,准确率还是蛮高的。

  关于资源瀑布图,还有优化空间,可以将 404 资源标红。同时也发现了静态资源请求错误没有记录的问题。

  去掉下面 if 语句中对 event.filename 的判断,因为资源错误是没有 filename 属性的,这样就能将此类资源错误记录在案了。

window.addEventListener(
  "error",
  function (event) {
    var errorTarget = event.target;
    // 过滤掉与业务无关的错误
    if (event.message === "Script error." || !event.filename) {
      return;
    }
    if (
      errorTarget !== window &&
      errorTarget.nodeName &&
      LOAD_ERROR_TYPE[errorTarget.nodeName.toUpperCase()]
    ) {
      handleError(formatLoadError(errorTarget));
    } else {
      handleError(
        formatRuntimerError(
          event.message,
          event.filename,
          event.lineno,
          event.colno,
          event.error
        )
      );
    }
  },
  true //捕获
);

  2022-12-09 在优化白屏后的几天,发现有误报的情况发生,因为 html 属性值中有内容。

  打开这些页面分析,发现有些内容的样式是绝对定位或固定定位,也就是说这些内容并不会撑起 body 的高度。

  那么要有高度,就需要等待其他元素渲染,若在上报白屏时,还没渲染成功,那么就有可能误报。

  为了验证自己的猜想,去查询了下某条性能记录的资源瀑布图,发现在触发 DOMContentLoaded 时,那些能撑起高度的资源还没加载完成。

  经测试发现,当因为脚本错误出现白屏时,两个事件的触发时机会很接近,而如果是正常情况,那么两者会有些时间的间隔。

  所以发生白屏时,也能减少因用户快速关闭页面而发生漏报的情况,因此最后决定将上报迁移到 load 事件中。

  2022-12-13 在监控白屏时,发现有一类的白屏是由标签栏切换引起的,因为在切换后会先将之前的列表清空,再去请求接口。

  在等待数据时就会有那么一段白屏时间差,为了体验好点,其实可以加一些过渡效果,例如加个 loading 等待。

点赞收藏
咖啡机KFJ

https://www.cnblogs.com/strick

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
4
3