[ 前端监控 SDK 开发 | 青训营笔记]

81 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天 什么是前端监控?

从一个经典面试题讲起:

在浏览器里,从输入URL到页面展示,这中间发生了什么?

前端监控就是尽可能的采集这一过程以及后续用户交互中产出的性能指标与发生的异常事件并上报到平台完成消费

为什么需要前端监控?

让我们从用户 (浏览网页的人) 的视角来抛出一些使用时遇到的问题

  • 这个网页打开好慢啊!
  • 为什么这个网页会那么卡啊!
  • 怎么网页的图片全都裂开了!
  • 这个网页怎么是白花花的一片什么都没有!

当我们的网页有了前端监控的能力

  • 打开好慢- 页面某个关键资源渲染太慢
  • 交互卡顿 - 页面同步计算任务太重,阻塞渲染
  • 资源加载失败- 客户端网络状态差,或上游服务节点异常
  • 页面白屏页面脚本执行失败、关键资源加载失败、请求失败等。

前端监控到底监控了什么?

  1. 性能指标
  2. 异常事件
  3. 用户行为

概述:前端监控之常用性能指标

web 性能标准的诞生

早期网页是纯静态的,但随着Web 爆发式发展,页面交互越来越复杂。开发者开始思考如何提高 Web 性能、改善用户体验

因此,2010 年 8 月,W3C 成立了 Web 性能工作组,由来自 Google 和 Microsoft 的工程师担任主席,目标是制定衡量 Web 应用性能的方法和 API。

随后,Web 性能工作组开始制定一系列 Web 性能标准,应用到桌面和移动浏览器以及其他环境中,帮助 Web 开发人员评估和理解应用的性能特征

用户为中心的性能指标

传统的性能指标专注于容易衡量的技术细节,但是它们很难反应出用户所真正关心的是什么如果你仅仅是把加载速度优化的更快,你很快就会发现网站的用户体验依然很差。

这就是创建用户为中心的性能指标的原因,它们专注于用户视角下的浏览体验

另外几个也很重要的指标

LCP (Largest Contentful Paint) :最大的内容在可视区域内变得可见的时间点。

TBT_(TotalBlocking Time): 量化主线程在空闲之前的繁忙程度,有助于理解在加

载期间,页面无法响应用户输入的时间有多久

CLS (Cumulative Layout Shift) : 量化了在页面加载期间,视口中元素的移动程度

当我们点击按钮时,突然出现了一块内容。无论是以一种增加意外点击几率的方式加载广告,还是在加载图片时文本向下移动,[内容的意外移动都会让人非常不舒服。

概述:前端监控之前端常见异常

静态资源错误

静态资源: 加载页面所需的 html、css 和 js 等文件,以及其他各类多媒体文件,如图片、音频和视频等

静态资源错误: 在拉取和加载静态资源的过程中发生了预期之外的错误,如网络异常等,导致静态资源无法正常渲染到页面上。

请求异常

状态码0是什么?

请求成功率 = 请求成功数/(请求成功数 + 请求失败数)

JS错误

在页面运行时发生的 Js 错误会严重影响页面的正常染与交互,是前端监控的重点

白屏异常

前面几类异常都可以通过浏览器提供的标准化方法来监听到,而白屏异化的监听方法,所以更考验前端监控开发者的功底

监听到白屏发生后,我们还需要对白屏的发生进行归因

通常导致白屏发生的原因可能有如下几点

  1. 发生 Js 错误导致关键资源渲染失败。
  2. 请求异常或静态资源加载失败
  3. 长时间的 Js 线程繁忙阻塞染任务

性能指标监控

利用PerformancePerformanceObserver可以监控到一些标准的渲染性能数据

/**
 * 列举出性能指标对应的 entry type
 * fp,fcp --> paint
 * lcp --> largest-contentful-paint
 * fip --> first-input
 */
const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']

// 1. 通过 PerformanceObserver 监听
const p = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    console.log(entry);
  }
})
p.observe({ entryTypes });
// 2. 也可以通过 window.performance 对象拿到 fp fcp 和 fip。
// 注意如果同步打印他们是取不到值的,想想为什么?
window.performance.getEntriesByType('paint');
window.performance.getEntriesByType('first-input');


// 3. 封装成一个 monitor高阶函数 report用来上报
function createPerfMonitor(report: ({ name: string, data: any }) => void) {
  const name = 'performance';
  const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
  function start() {
    const p = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        report({ name, data: entry });
      }
    })
    p.observe({ entryTypes });
  }
  return { name, start }
}

JS错误监控

利用 window.addEventListener 的和 unhandledrejection可以监error 控到全局的 js 错误

// 1. 监听 js 执行报错
window.addEventListener("error", (e) => {
  // 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
  if (e.error) {
    console.log('caputure an error', e.error);
  }
});
// throw(new Error('test'));
// 2. 监听 promise rejection
window.addEventListener("unhandledrejection", (e) => {
  console.log('capture a unrejection', e);
});
Promise.reject('test');

// 3. 封装成一个 monitor
function createJsErrorMonitor(report: ({ name: string, data: any }) => void) {
  const name = "js-error";
  function start() {
    window.addEventListener("error", (e) => {
      // 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
      if (e.error) {
        report({ name, data: { type: e.type, message: e.message } });
      }
    });
    window.addEventListener("unhandledrejection", (e) => {
      report({ name, data: { type: e.type, reason: e.reason } });
    });
  }
  return { name, start }
}

静态资源错误监控

// 1. 监控静态资源错误,注意需要在捕获阶段才能监听到
window.addEventListener('error', e => {
  // 注意区分 js error
  const target = e.target || e.srcElement;
  if (!target) {
    return
  }
  if (target instanceof HTMLElement) {
    let url;
    // 区分 link 标签,获取静态资源地址
    if (target.tagName.toLowerCase() === 'link') {
      url = target.getAttribute('href');
    } else {
      url = target.getAttribute('src');
    }
    console.log('异常的资源', url);
  }
}, true)
const link = document.createElement("link");
link.href = "1.css";
link.rel = "stylesheet";
document.head.append(link);

const script = document.createElement("script");
script.src = "2.js";
document.head.append(script);

// 2. 封装成一个 monitor
function createResourceErrorMonitor(report: ({ name: string, data: any }) => void) {
  const name = "resource-error";
  function start() {
    window.addEventListener('error', e => {
      // 注意区分 js error
      const target = e.target || e.srcElement;
      if (!target) {
        return
      }
      if (target instanceof HTMLElement) {
        let url;
        // 区分 link 标签,获取静态资源地址
        if (target.tagName.toLowerCase() === 'link') {
          url = target.getAttribute('href');
        } else {
          url = target.getAttribute('src');
        }
        report({ name, data: { url } });
      }
    }, true)
  }
  return { name, start }
}

请求异常监控

通过 hook xhr 和fetch 对象来监听请求时发生的错误。

// 1. 写一个简易的 hook 函数
function hookMethod(
  obj: any,
  key: string,
  hookFunc: Function,
) {
  return (...params: any[]) => {
    obj[key] = hookFunc(obj[key], ...params)
  }
}


// 2. hook xhr 对象的 open 方法拿到请求地址和方法
hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
  function (method: string, url: string) {
    this.payload = {
      method,
      url,
    };
    // 执行原函数
    origin.apply(this, [method, url]);
  }
)();

// 3. hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
  function (...params: any[]) {
    this.addEventListener("readystatechange", function () {
      if (this.readyState === 4 && this.status >= 400) {
        this.payload.status = this.status;
        console.log(this.payload);
      }
    });
    origin.apply(this, params);
  }
)();

const xhr = new XMLHttpRequest();
xhr.open("post", "111.cc");
xhr.send();

// 4, 封装成一个 monitor
function createXhrMonitor(report: ({ name: string, data: any }) => void) {
  const name = "xhr-error";
  function hookMethod(
    obj: any,
    key: string,
    hookFunc: Function,
  ) {
    return (...params: any[]) => {
      obj[key] = hookFunc(obj[key], ...params)
    }
  }
  function start() {
    hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
      function (this, method: string, url: string) {
        this.payload = {
          method,
          url,
        };
        origin.apply(this, [method, url]);
      }
    )();
    hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
      function (this, ...params: any[]) {
        this.addEventListener("readystatechange", function () {
          if (this.readyState === 4 && this.status >= 400) {
            this.payload.status = this.status;
            report({ name, data: this.payload });
          }
        });
        origin.apply(this, ...params);
      }
    )();
  }
  return { name, start }
}

总结: 前端监控可以方便开发者发现程序错误的问题位置,更有利于调整bug