打造自己的前端监控---前端接口监控

前言

上一篇我们分享了如何进行前端的错误采集,今天我们来分析一下如何进行前端接口的采集。

数据结构

typescript 复制代码
export interface IApiContext {
  version: string, // 组件版本号
  globalSessionId: string, // 全局会话标识
  uniqueVisitorId: string, // 独立访客标识
  systemName: string, // 系统简称
  serviceName: string, // 服务名称
  referer?: string, // 引用
  userAgent: string, // 浏览器用户代理(浏览器标识)
  ip?: string, // ip地址
  generateTime: string | number, // 发生时间
  url: string, // 页面地址
  domain: string, // 页面域名
  page:string,
  title: string, // 页面标题
  reqURL: string, // 请求地址
  reqParam?: string, // 请求参数
  reqMsg?: string, // 请求报文
  reqTime: string | number,// 请求时间戳
  httpStatusCode?:string | number,
  respCode?: string, // 响应吗
  respDesc?: string, // 响应描述
  respMsg?: string, // 响应报文
  respTime?: string | number // 响应时间戳
}

如何实现采集

  • 重写xhr的Open方法、Send方法

  • 监听loadend事件

这些操作都是在原型上来完成的,其实这样操作我觉得类似切面(AOP),我门能在这里拦截所有的请求,能获取到所有请求的状态码,参数,计算请求耗时,响应时间-请求时间(当然这个耗时仅供参考),要想获取准确的耗时需要使用浏览器的performance里面的resource中的耗时,这里不在做过多描述,我们在其他文章里已经全面说过这个耗时。

同时我们为了前后端日志串联,在我们拦截的所有请求中添加自定义请求头,这个请求头的值是一个唯一的uuid,这样根据这个uuid可以把前后端日志进行串联,实现全端的链路追踪。

js 复制代码
export const ajaxInterceptorHandler = (options: IParams) => {

  try {
    if (!XMLHttpRequest) {
      return;
    }
    let originalXhrProto = XMLHttpRequest.prototype;
    let reqUrl: string = "";
    let xhrInstance = null;
    // @ts-ignore
    // 可以拿到请求信息
    replaceOld(originalXhrProto, EVENTTYPES.OPEN, (originalOpen: voidFun): voidFun => {
      return function (this: BTRACEXMLHttpReques, ...args: any[]): void {
        xhrInstance = this;
        xhrInstance.btrace = {
          url: args[1],
          reqId: uuid(),
          sendTime: getTimeStamp()
        };
        reqUrl = reqUrlTool(args[1]);
        originalOpen.apply(this, args);
      }
    })
    replaceOld(originalXhrProto, EVENTTYPES.SEND, (originalSend: voidFun): voidFun => {
      return function (this: BTRACEXMLHttpReques, ...args: any[]): void {
        isCludeUrl(this.btrace.url, options) && xhrInstance.setRequestHeader(HEADERS.XB3SESSIONId, getCookie(HEADERS.XB3SESSIONId));
        on(this, EVENTTYPES.LOADEND, function (this: BTRACEXMLHttpReques){
          if (options.apiRequestIsEnable) {
            if (isCludeUrl(this.btrace.url, options) && !this.btrace.timeout) {
               this.btrace.resTime = getTimeStamp();
               this.btrace.url = reqUrlTool(this.btrace.url);
               this.btrace.duration = this.btrace.resTime - this.btrace.sendTime;
               this.btrace.reqParam = getReqParams(this.btrace.url);
               createApiMsgData(xhrInstance, xhrInstance, options, ERROR_TYPE.AJAX);
            }

          }
        });
        on(this, EVENTTYPES.TIMEOUT, function (this: BTRACEXMLHttpReques){
          this.btrace.resTime = getTimeStamp();
          if (this && this.type === EVENTTYPES.TIMEOUT) {
            this.btrace.timeout = true;
            this.btrace.url = reqUrl;
            this.btrace.duration = this.btrace.resTime - this.btrace.sendTime;
            this.btrace.reqParam = getReqParams(this.btrace.url);
            createApiMsgData(xhrInstance, xhrInstance, options, ERROR_TYPE.AJAX);
          }
        })
        originalSend.apply(this, args);
      }
    })

  } catch (e) {
    console.warn("btrace:" + e);
  }

}
  • 重写fetch请求
js 复制代码
export const fetchInterceptorHandler = (options: IParams) => {

  try {

    if (!_global.fetch) {
      return;
    }

    replaceOld(_global, EVENTTYPES.FETCH, (originalFetch: voidFun) => {
      return function (url: string, config: Partial<RequestInit> = {}): void {
        let sendTime = getTimeStamp(); // 发送时间
        let reqId = uuid();
        const headers = config.headers || {};
        if (isCludeUrl(url, options)) {
          if (config && 'headers' in config) {
            headers[HEADERS.XB3SESSIONId] = getCookie(HEADERS.XB3SESSIONId);
          } else {
            let customHeader = {
              headers: {
                [HEADERS.XB3SESSIONId]: getCookie(HEADERS.XB3SESSIONId)
              }
            };
            config.headers = customHeader.headers;
          }
        }

        return originalFetch.apply(_global, [url, config]).then(
          (res: Response) => {
            let resTime = getTimeStamp();
            let duration = resTime - sendTime;
            const tempRes = res.clone();
            tempRes["btrace"] = {
              resTime: resTime,
              sendTime: sendTime,
              duration: duration,
              reqParam: getReqParams(url),
              reqId: reqId
            };
            if (options.apiRequestIsEnable) {
              createApiMsgData(tempRes, tempRes, options, ERROR_TYPE.FETCH)
            }

            return res;
          },
          (e: Error) => {
            if (options.apiRequestIsEnable) {
              if (isCludeUrl(url, options)) {
                e["status"] = 0;
                let resTime = getTimeStamp();
                let duration = resTime - sendTime;
                e["btrace"] = {
                  resTime: resTime,
                  sendTime: sendTime,
                  duration: duration,
                  reqParam: getReqParams(url),
                  reqId: reqId
                };
                createApiMsgData(e, e, options, ERROR_TYPE.FETCH)
              }
            }
            throw e;
          }
        )
      }
    })
  } catch (e) {
    console.log('ee', e);
  }

}

总结

基于以上方式我们完成了对请求的拦截并添加了我们自己的需求,在这里获取请求状态码、计算请求耗时、请求的唯一标识(这个标识能起大作用)。我们也完成了线上环境的验证,目前正在推广使用中。

相关推荐
Java 码农1 小时前
nodejs koa留言板案例开发
前端·javascript·npm·node.js
ZhuAiQuan1 小时前
[electron]开发环境驱动识别失败
前端·javascript·electron
nyf_unknown1 小时前
(vue)将dify和ragflow页面嵌入到vue3项目
前端·javascript·vue.js
胡gh1 小时前
数组开会:splice说它要动刀,map说它只想看看。
javascript·后端·面试
胡gh1 小时前
浏览器:我要用缓存!服务器:你缓存过期了!怎么把数据挽留住,这是个问题。
前端·面试·node.js
你挚爱的强哥2 小时前
SCSS上传图片占位区域样式
前端·css·scss
奶球不是球2 小时前
css新特性
前端·css
Nicholas682 小时前
flutter滚动视图之Viewport、RenderViewport源码解析(六)
前端
无羡仙2 小时前
React 状态更新:如何避免为嵌套数据写一长串 ...?
前端·react.js
TimelessHaze2 小时前
🔥 一文掌握 JavaScript 数组方法(2025 全面指南):分类解析 × 业务场景 × 易错点
前端·javascript·trae