如何设计一个监听http请求的埋点


前言

为了更好地进行数据统计,更加深入地获取应用运行的状态,监控线上异常。

我们想做一个针对 http 数据请求的埋点监听。通过监听应用的中的 http 数据请求,和往常业务数据进行对比,从而实现对线上业务的异常监控。

目标

为了实现这个线上监控的目的,我们要先制定需要采集的数据。

目前主流采集的数据有两个思考方向,一是从响应速度,二是响应质量。响应速度方面,我们经常采集的数据有,http 请求时间、结束时间、持续时间;响应质量这块,我们常采集的数据有 http 状态码、请求的接口、当前请求发出的页面。

方案设计

无论你是用当前流行的SWR或者axios请求库,亦或是用例如next.jsumi.js自带的请求库。其底层异步请求都是xhrfetch支撑的。

所以我们仅需要从xhrfetch入手就能实现的我们的目的。

xhr全称是XMLHttpRequest,它利用这个对象进行 ajax 请求。一个XMLHttpRequest对象在发送请求时,必定要经过创建请求实例、发送请求这两个阶段。而这两个阶段又分别对应它实例里的opensend方法,在这里我们可以选择其中一个阶段进行我们的埋点操作。这个例子中我选择在open阶段进行计时。

到这一步,思路就很清晰了。我们可以直接在XMLHttpRequest对象的原型上重写open方法,来实现我们的目的。

fetch是基于Promise的异步网络请求,它的底层并不使用XMLHttpRequest对象,所以对它我们需要额外进行处理。

根据上面封装xhr的思路,我们可以对fetch进行重写,把它重新包装成一个Promise来达到我们的目的。

具体代码如下:

let isCalled = false;
const sendData = data => data;

const httpMonitor = () => {
  if (isCalled) return;
  isCalled = true;

  // 处理XHR请求
  if (XMLHttpRequest?.prototype?.open) {
    const originOpen = XMLHttpRequest.prototype.open;

    const start = new Date().getTime();

    XMLHttpRequest.prototype.open = function (...args) {
      this.addEventListener('loadend', () => {
        const end = new Date().getTime();
        const data = {
          start,
          end,
          duration: end - start,
          httpStatus: this.status,
          reqUrl: args[1],
          reqPage: window?.location?.href
        };
        sendData(data);
      });

      this.addEventListener('timeout', () => console.log('xhr timeout'));

      originOpen.apply(this, args);
    };
  }

  // 处理fetch请求
  if (window?.fetch) {
    const originFetch = window.fetch;

    window.fetch = function (...args) {
      const start = new Date().getTime();
      let end = start;

      return new Promise((resolve, reject) => {
        originFetch.apply(this, args).then(
          response => {
            end = new Date().getTime();
            resolve(response);
          },
          error => {
            end = new Date().getTime();
            reject(error);
          }
        );
      }).then(
        res => {
          const { code = 200, url = '' } = res || {};
          const data = {
            start,
            end,
            duration: end - start,
            httpStatus: code,
            reqUrl: url,
            reqPage: window?.location?.href
          };

          sendData(data);
          return res;
        },
        err => err
      );
    };
  }
};

httpMonitor();

export { httpMonitor };