前端监控(3) | 青训营笔记

72 阅读4分钟

监控前端性能与异常

这是我参与「第五届青训营」笔记创作活动的第15天.上一篇文章前端监控(2) | 青训营笔记 - 掘金 (juejin.cn)中了解了前端监控的常见异常, 今天主要学习如何编写代码实现性能与异常的监控.

性能指标监控

利用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);
  }
})
// observe获取需要的性能值
p.observe({ entryTypes });
​
// 2. 也可以通过 window.performance 对象拿到 fp fcp 和 fip。
// 注意如果同步打印他们是取不到值的
// 渲染前已经同步执行了JS, 因此输出时还没获得值. 应该在渲染完之后输出
console.log(window.performance.getEntriesByType('paint'));
window.performance.getEntriesByType('first-input');
​
// 因此实战时优先从performance中拿,没有才会从PerformanceObserver拿

把监听能力封装成一个monitor

// 希望满足以下要求
// (1)起名字,唯一标识
// (2)具有监听能力
// (3)主动开启,而不是被动开启(创建出来就马上开启)
// (4)上报能力,把监听到的数据上报function createPerfMonitor(report: ({ name: string, data: any }) => void) { // 高阶函数,函数里传入report函数
  const name = 'performance';
  const entryTypes = ['paint', 'largest-contentful-paint', 'first-input']
  // 调用start才会开启监听能力
  function start() {
    const p = new PerformanceObserver(list => {
      for (const entry of list.getEntries()) {
        report({ name, data: entry }); //上报数据
      }
    })
    p.observe({ entryTypes });
  }
  // 监听的名字和start方法
  return { name, start }
}

JS错误监控

利用EventTarget.addEventListener的error和unhandledrejection 可以监控到全局的JS错误。

一般在根节点或全局对象监听,即window,因此也可以改写成window.addEventListener

// 1. 监听 js 执行报错
window.addEventListener("error", (e) => { // e表示一个事件
  // 只有 error 属性不为空的 ErrorEvent 才是一个合法的 js 错误
  if (e.error) {
    console.log('caputure an error', e.error);
  }
});  
throw(new Error('test'));

执行上方代码可以得到下述输出。

image-20230214135521103.png

test为错误名称,下面是错误堆栈

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

执行上述代码,得到下面输出

image-20230214135547425.png

将上面两个功能封装成一个monitor。封装过程的思路与性能指标中一致。

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) => {
      // 错误类型,错误原因。通过type来区分两类错误
      report({ name, data: { type: e.type, reason: e.reason } });
    });
  }
  return { name, start }
}

静态资源错误监控

利用window.addEventListener的error事件可以监控到静态资源错误,注意要和JS error进行区分。

静态资源报错在属性上具有明显的特征,若target/ srcElement 属性是空的,则不是静态资源。

// 1. 监控静态资源错误,注意需要在捕获阶段才能监听到
window.addEventListener('error', e => {
  // 区分 js error
  const target = e.target || e.srcElement; //为了兼容新旧浏览器,加上srcElement
  // 非静态
  if (!target) {
    return
  }
  // target继承HTMLElement,可能是通过link/script标签创建的静态资源,要区分
  
  if (target instanceof HTMLElement) {
    let url; //静态资源指向地址
    // 区分 link 标签,获取静态资源地址
    if (target.tagName.toLowerCase() === 'link') {
      // Link
      url = target.getAttribute('href');
    } else {
      // Script
      url = target.getAttribute('src');
    }
    console.log('异常的资源', url); //出错的静态资源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);

注意:监控静态资源错误,注意需要在捕获阶段才能监听到。但是addEventListener的useCapture属性默认为false,因此要主动设置为true。

封装成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 XMLHttpRequest,xhrfetch对象来监听请求时发生的错误。

  1. 写一个简易的 hook(钩子) 函数

想在原有的逻辑上注入自己的逻辑,从而实现监听,注入等能力。

function hookMethod(//高阶函数
  //三个参数
  obj: any,
  key: string,
  hookFunc: Function,
) {
  //返回值也是一个函数
  return (...params: any[]) => {
    obj[key] = hookFunc(obj[key], ...params)
  }
}

xhr : open-> send 两步完成后请求才生效

  1. hook xhr 对象的 open 方法拿到请求地址和方法

在原型对象上做hook

hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
  function (method: string, url: string) { // 即原始open的入参
    // this指向原始xhr
    this.payload = {
      method,
      url,
    };
    // 执行原函数(保证在执行自己逻辑时不破坏原逻辑)
    origin.apply(this, [method, url]);
  }
)();
  1. hook xhr 对象的 send 方法监听到错误的请求
hookMethod(XMLHttpRequest.prototype, 'send', (origin: Function) =>
  function (...params: any[]) {
    this.addEventListener("readystatechange", function () {
      // ==4:请求完成,状态码>=400发生异常
      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();
  1. 封装成monitor
function createXhrMonitor(report: ({ name: string, data: any }) => void) {
  const name = "xhr-error";
  // hook
  function hookMethod(
    obj: any,
    key: string,
    hookFunc: Function,
  ) {
    return (...params: any[]) => {
      obj[key] = hookFunc(obj[key], ...params)
    }
  }
  
  function start() {
   //  get 
   hookMethod(XMLHttpRequest.prototype, 'open', (origin: Function) =>
      function (this, method: string, url: string) {
        this.payload = {
          method,
          url,
        };
        origin.apply(this, [method, url]);
      }
    )();
 //send  
  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);
      }
    )();
  }
  // 返回start
  return { name, start }
}