前端监控与实战 | 青训营笔记

111 阅读5分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 13 天

前端监控

什么是前端监控?

一般来讲一个成熟的产品,运营与产品团队需要关注用户在产品内的行为记录,通过用户的行为记录来优化产品,研发与测试团队则需要关注产品的性能以及异常,确保产品的性能体验以及安全迭代。

前端监控的重要性

性能监控(监控页面性能)

  • 不同用户,不同机型和不同系统下的首屏加载时间
  • 白屏时间
  • http 等请求的响应时间
  • 静态资源整体下载时间
  • 页面渲染时间
  • 页面交互动画完成时间,等...

这些性能监控的结果,可以展示前端性能的好坏,根据性能监测的结果可以进一步的去优化前端性能,尽可能的提高用户体验。

当我们没有前端监控时,我们面对的问题的解决方案: image.png

当我们利用前端监控时:

image.png

以用户为中心的性能指标

创建以用户为中心的性能指标,可以让我们专注于用户视角下的浏览体验。

image.png

当用户打开一个网页时,会经历三个重要结点。

  • FP: 首次渲染结点
  • FCP: 首次有内容渲染结点
  • FMP:绘制有意义的时间点。

image.png

  • TTI: 测量用户能开始操作页面的时间
  • FID:测量用户第一次页面交互直到浏览器做出反应的时间

image.png

  • LCP: 最大内容在可视区域内变得可见的时间点

image.png

常见的监控异常

静态资源异常

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

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

image.png

请求异常

请求异常和我们的状态码相关,一般当我们的状态码>=400就可以归类于请求异常。

  • 2xx:成功类型,例:200 OK。
  • 3xx: 重定向类型,例:301 永久重定向,302 暂时重定向
  • 4xx:客户端错误类型:例:400 错误请求,401 授权错误,403 服务器理解请求但拒绝请求
  • 5xx:服务端错误类型,例:500 内部错误,501 服务不可用

状态码为0的情况,即意味着xhr无法请求执行。

image.png

JS异常

在页面运行时发生JS错误会严重影响页面的正常交互和渲染。

image.png

白屏异常

白屏异常没有标准的监听方法,所以更考验前端开发者的功底。

通常我们可以用DOM树来粗略判断是否发生。

image.png

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

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

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

监控

前端性能指标监控

我们可以用 pefformancepefformanceObserver 来监控

image.png

**
 * 列举出性能指标对应的 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
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 }
}

静态资源错误监控

利用window.addEventListener的error事件监听.

// 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 }
}

请求异常监控

通过用钩子的方式,来监听xhr发生的错误.

// 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 }
}

按需加载前端监控

function createSdk(url: string) {
  const monitors: Array<{ name: string, start: Function }> = [];
  const sdk = {
    url,
    report,
    loadMonitor,
    monitors,
    start,
  }
  function report({ name: string, data: any }) {
    // 注意:数据发送前需要先序列化为字符串
    navigator.sendBeacon(url, JSON.stringify({ name: string, data: any }));
  }
  function loadMonitor({ name: string, start: Function }) {
    monitors.push({ name: string, start: Function });
    // 实现链式调用
    return sdk;
  }
  function start() {
    monitors.forEach(m => m.start());
  }
  return sdk;
}
const sdk = createSdk("111.com");
const jsMonitor = createJsErrorMonitor(sdk.report);
sdk.loadMonitor(jsMonitor).loadMonitor(createPerfMonitor(sdk.report));
sdk.start();
throw (new Error('test'));



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 }
}

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 }
}