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

52 阅读3分钟

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

前端监控简介

前端监控即采集用户访问交互过程中产生的性能指标和发生的异常事件,并将其上报到平台进行消费

通过对页面数据的采集和上报,来帮助开发者更快地对质量差的页面进行分析和归因

  • 性能指标
  • 异常事件
  • 用户行为

前端监控内容的性能指标

传统性能指标

以用户为中心的性能指标

  • FP 首次渲染时间点
  • FCP 首次有内容渲染时间点
  • FMP 首次绘制有意义内容时间点
  • TTI 页面从开始加载到主要子资源完成渲染并能够响应用户输入所需的时间 (可反映页面可用性)
  • SI 页面可视区域加载速度(可检测页面加载体验差异)
  • FID 用户首次与页面发生交互至浏览器对交互做出响应并开始处理事件时处理程序所经过的时间

  • LCP 最大内容在可视区域内变得可见的时间点(类似 FMP,但更易生成)
  • TBT 量化主线程在空闲之前的繁忙程度(可反映加载期间页面无法响应用户输入的时间长短)
  • CLS 量化页面加载过程期间,视口中元素的移动程度

前端监控内容的异常

  • 静态资源错误

在拉取和加载静态资源的过程中发生了意料之外的错误,导致静态资源无法正常渲染到页面上

  • 请求异常

请求状态码 >= 400 的可视为请求异常

(可以包含通过异步请求拉取的静态资源错误)

请求成功率 = 请求成功数 / 请求总数

  • JS 错误

页面运行中产生的 JS 错误可能会严重影响页面的正常渲染和交互

  • 白屏异常

可通过判断 DOM 树的结构来简单的判断是否发生白屏

发生 JS 错误导致关键资源渲染失败

请求异常或静态资源加载失败

长时间的 JS 线程繁忙阻塞渲染任务

监控前端性能及异常实例

性能监控

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 错误监控

function createJsErrorMonitor(report: ({ name: string, data: any }) => void) {
  const name = "js-error"

  function start() {
    window.addEventListener("error", (e) => {
      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 createResourceErrorMonitor(report: ({ name: string, data: any }) => void) {
  const name = "resource-error"

  function start() {
    window.addEventListener('error', e => {
      const target = e.target || e.srcElement
      if (!target) {
        return
      }
      if (target instanceof HTMLElement) {
        let url;
        if (target.tagName.toLowerCase() === 'link') {
          url = target.getAttribute('href')
        } else {
          url = target.getAttribute('src')
        }
        report({ name, data: { url } })
      }
    }, true)
  }

  return { name, start }
}

请求异常监控

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

封装通用前端 SDK

数据采集=>组装上报=>清洗存储=>数据消费

上报用 API

navigator.sendBeacon()

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

function createJsErrorMonitor(report: ({ name: string, data: any }) => void) {
  const name = "js-error";
  function start() {
    window.addEventListener("error", (e) => {
      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 }
}

const sdk = createSdk("111.com");
const jsMonitor = createJsErrorMonitor(sdk.report);
sdk.loadMonitor(jsMonitor).loadMonitor(createPerfMonitor(sdk.report));
sdk.start();
throw (new Error('test'));

拓展

  • 其他性能指标 FMP、CLS、TTI
  • 慢请求
  • hook 函数支持 unhook
  • 用户行为监控