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

前言

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

数据结构

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

}

总结

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

相关推荐
Mintopia3 分钟前
无界微前端:父子应用通信、路由与状态管理最佳实践
架构·前端框架·全栈
前端老宋Running4 分钟前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔4 分钟前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654266 分钟前
Android的自定义View
前端
WILLF7 分钟前
HTML iframe 标签
前端·javascript
枫,为落叶24 分钟前
Axios使用教程(一)
前端
小章鱼学前端29 分钟前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah30 分钟前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝32 分钟前
手搓一个简简单单进度条
前端
uup36 分钟前
JavaScript 中 this 指向问题
javascript