【三】性能优化&前端监控_接口请求监控

375 阅读3分钟

接口监控

在 js 页面中,接口请求的方式其实总共只分为两种:

  1. XMLHttpRequest
  2. Fetch

因此,接口的监控其实就是需要为了兼容这两种监控的情况

接口监控的关注点及实现的基本思路

如果要进行接口监控,我们可能会关注下面这些指标:

  1. 接口的成功率
  2. 接口的请求耗时
  3. 接口请求的 UV/PV
  4. 其他指标....

针对这些内容,我们需要怎么做才能够获取这些指标呢?

如果之前有做过埋点的同学应该会很快反应过来,这种情况下,我们只需要在接口请求成功之后发送一个埋点,然后根据不同的场景去建立报表即可(本质上就是一个数据库查询的过程)

因此,接口的监控我们本质上只需要在接口完成时发送一个埋点即可,埋点中的参数存在着 业务/开发 关心的内容

接口监控方式

XMLHttpRequest

// @ts-ignore
!(function () {
  const XMLHttpRequestPrototype = XMLHttpRequest.prototype;
  // 由于我们需要重写 XMLHttpRequest.open, XMLHttpRequest.send, 这里我们将这两个函数缓存一下
  const XMLHttpRequestOpen = XMLHttpRequestPrototype.open;
  const XMLHttpRequestSend = XMLHttpRequestPrototype.send;

  /**
   *
   * @param {Function} paramsFormat XMLHttpRequest 接口上报参数格式化函数
   *
   * 注意点: 如果需要监听异常的情况,可以直接在 paramsFormat 这个参数中处理
   *
   * Q: 这里为什么需要提供一个 paramsFormat 呢?(为什么这里需要这么设计)
   * A:
   * 在监听的时候,作为监控 SDK 的维护方,我们不可能枚举所有的监控参数和监控场景(如果所有参数都写入埋点对于数据库存储时不友好的,会增加存储成本)
   * 因此这里我们可以将参数的上报格式交由具体的开发者去管理,我们只需要关注上报即可
   * 同时这里我们直接把 response 返回给开发者了,则接口请求的成功与否直接交由给开发者维护了
   */
  const XMLHttpRequestTimeScript = (
    paramsFormat = (response = { diff: 0 }) => ({ diff: response.diff })
  ) => {
    XMLHttpRequestPrototype.open = function (...args) {
      // 将 method url 写入到 this 对象中,后续我们上报耗时需要这个内容
      // 这里可以使用对象进行变量缓存,也可以使用闭包进行对象缓存
      this.method = args[0];
      this.url = args[1];

      // 调用 XMLHttpRequest open
      XMLHttpRequestOpen.apply(this, args);
    };

    XMLHttpRequestPrototype.send = function (...args) {
      // 请求的开始时间
      this.startTime = Date.now();

      const loadendCallback = () => {
        // 请求的结束时间
        this.endTime = Date.now();
        // 请求耗时
        const diff = this.endTime - this.startTime;

        const params = paramsFormat({
          ...this.response,
          ...args,
          url: this.url,
          method: this.method,
          startTime: this.startTime,
          endTime: this.endTime,
          diff,
        });
        console.log(
          `请求方法: ${this.method}; 请求路径: ${this.url}; 请求耗时: ${diff}; \n监控参数为`,
          params
        );

        this.removeEventListener('loadend', loadendCallback, true);
      };

      // send 之后我们绑定 loadend 回调事件
      this.addEventListener('loadend', loadendCallback, true);
      // 我们还需要重新调用 XMLHttpRequestPrototype.send 方法,这样请求才能够正常发送
      XMLHttpRequestSend.apply(this, args);
    };
  };

  XMLHttpRequestTimeScript((response) => ({
    url: response.url,
    method: response.method,
    startTime: response.startTime,
    endTime: response.endTime,
    diff: response.diff,
  }));
})();

demo

Q: XMLHttpRequestTimeScript 脚本中为什么需要提供一个 paramsFormat 呢?(为什么这里需要这么设计)

A:
在监听的时候,作为监控 SDK 的维护方,我们不可能枚举所有的监控参数和监控场景(如果所有参数都写入埋点对于数据库存储时不友好的,会增加存储成本)
因此这里我们可以将参数的上报格式交由具体的开发者去管理,我们只需要关注上报即可
同时这里我们直接把 response 返回给开发者了,则接口请求的成功与否直接交由给开发者维护了

在第 3 方页面测试脚本

测试方案: 暴力猴

测试链接: 掘金首页

XMLHttpRequest_script.png

Fetch

// @ts-ignore
!(function () {
  const originalFetch = window.fetch;

  /**
   *
   * @param {Function} paramsFormat XMLHttpRequest 接口上报参数格式化函数
   *
   * 注意点: 如果需要监听异常的情况,可以直接在 paramsFormat 这个参数中处理
   *
   * Q: 这里为什么需要提供一个 paramsFormat 呢?(为什么这里需要这么设计)
   * A:
   * 在监听的时候,作为监控 SDK 的维护方,我们不可能枚举所有的监控参数和监控场景
   * 因此这里我们可以将参数的上报格式交由具体的开发者去管理,我们直观上报即可
   * 同时这里我们直接把 response 返回给开发者了,则接口请求的成功与否直接交由给开发者维护了
   */
  const initFetch = (
    paramsFormat = (response = { diff: 0 }) => ({ diff: response.diff })
  ) => {
    window.fetch = function newFetch(url, config) {
      // 开始时间
      const startTime = Date.now();

      return originalFetch(url, config)
        .then((res) => {
          // 结束时间
          const endTime = Date.now();
          // 请求耗时
          const diff = endTime - startTime;

          const params = paramsFormat({
            ...res,
            ...config,
            url,
            startTime: startTime,
            endTime: endTime,
            diff,
          });
          console.log(
            `请求方法: ${config.method}; 请求路径: ${url}; 请求耗时: ${diff}; \n监控参数为`,
            params
          );

          return res;
        })
        .catch((err) => {
          console.log('err: ', err);

          throw err;
        });
    };
  };

  initFetch((response) => ({
    url: response.url,
    method: response.method,
    startTime: response.startTime,
    endTime: response.endTime,
    diff: response.diff,
  }));
})();

demo

Fetch 的思路和设计基本上和 XMLHttpRequest 是一致的

接口监控的原理

XMLHttpRequest 和 Fetch 的原理本质上就是重写浏览器提供的 XMLHttpRequest,Fetch 对象,添加一些我们定义的回调函数 具体可以参见实现的脚本

参考链接

系列&demo 相关

  1. github blog
  2. demos 仓库
  3. 浏览器渲染流程
  4. 测试 url - ieubs
  5. 测试 url - 交易猫

参考资料

  1. 深入了解前端监控原理
  2. 前端监控 SDK 的一些技术要点原理分析
  3. 腾讯二面:现在要你实现一个埋点监控 SDK,你会怎么设计?