skywalking-client-js前端监控实现分析(零基础搞懂前端监控)

Posted 前端呆头鹅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了skywalking-client-js前端监控实现分析(零基础搞懂前端监控)相关的知识,希望对你有一定的参考价值。

本文基于skywalking-client-js源码做前端监控实现分析,建议下载其源码对比观看。

源码代码分为入口和功能代码两部分,入口提供不同的调用方式,功能代码是通过入口代码调用的。

入口文件位于skywalking-client-jssrc/monitor.ts

功能代码位于src中skywalking-client-js/errors/performance/services/traces四个文件夹下。

更多前端知识,敬请关注。

一 监控入口

ClientMonitor下面有五个方法,可以通过调用这些方法来使用client.js。

1.1 setPerformance()

  1. 参数在setPerformance中处理后,传入performance函数。
  2. performance函数对情况进行判断,在合适的时机触发处理函数。
  3. 先判断页面是否加载完成,是则触发处理函数,否则添加 load监听事件,触发处理函数。
  4. 然后判断是否进行单页面应用监控,是则添加hashchange监听事件,触发处理函数。

参数处理

给出默认配置对象customOptions,传入对象可以覆盖默认项的对应项,无传入的情况下使用默认值。

  customOptions: {
    collector: location.origin, // report serve
    jsErrors: true, // vue, js and promise errors
    apiErrors: true,
    resourceErrors: true,
    autoTracePerf: true, // trace performance detail
    useFmp: false, // use first meaningful paint
    enableSPA: false,
    traceSDKInternal: false,
    detailMode: true,
    noTraceOrigins: [],
    traceTimeInterval: 60000, // 1min
  }
  ---
  setPerformance(configs: CustomOptionsType) {
   	this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
    this.performance(this.customOptions);
  },

事件调取

该方法在参数处理后调取performance事件,该部分在功能代码-performance中分析。

重要参数

enableSPA 为是时开启单页面应用性能监控。

相关知识

单页面应用

通过url - hash变化来进行页面转换效果,这里对hash进行事件监控,在变化的时候触发事件。

Document.readyState 属性

  • loading(正在加载)document 仍在加载。
  • interactive(可交互)文档已被解析,"正在加载"状态结束,但是诸如图像,样式表和框架之类的子资源仍在加载。
  • complete(完成)文档和所有子资源已完成加载。表示 load (en-US) 状态的事件即将被触发。

1.2 register()

register入口是一个综合性入口,通过此方法调用,会调用功能代码中的catchErrors()、performance()、traceSegment()部分。

  register(configs: CustomOptionsType) {
    this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
    this.catchErrors(this.customOptions);
    if (!this.customOptions.enableSPA) {
      this.performance(this.customOptions);
    }

    traceSegment(this.customOptions);
  }

见功能代码-catchErrors,功能代码-performance,功能代码-traceSegment。

重要参数

enableSPA 为false时开启性能监控。

1.3 catchErrors()

重要参数

jsErrors 是否监听js和promise错误

apiErrors 是否监听ajaxErrors错误

resourceErrors 是否监听资源错误

事件调取

  catchErrors(options: CustomOptionsType) {
    const { service, pagePath, serviceVersion, collector } = options;

    if (options.jsErrors) {
      JSErrors.handleErrors({ service, pagePath, serviceVersion, collector });
      PromiseErrors.handleErrors({ service, pagePath, serviceVersion, collector });
      if (options.vue) {
        VueErrors.handleErrors({ service, pagePath, serviceVersion, collector }, options.vue);
      }
    }
    if (options.apiErrors) {
      AjaxErrors.handleError({ service, pagePath, serviceVersion, collector });
    }
    if (options.resourceErrors) {
      ResourceErrors.handleErrors({ service, pagePath, serviceVersion, collector });
    }
  }

见功能代码 - handleErrors

二 功能代码

2.1 performance()

  1. performance函数对情况进行判断,在合适的时机触发处理函数。
  2. 先判断页面是否加载完成,是则触发处理函数,否则添加load监听事件,触发函数,获取数据。
  3. 然后判断是否进行单页面应用监控,是则添加hashchange监听事件,触发函数,获取数据。
  4. 发送性能数据到服务端

处理-整合性能数据

tracePerf.recordPerf是一个async异步函数。

在该函数中通过await异步取得了性能数据对象和fmp对象。

根据配置将数据整合后触发提交函数。

代码展示

  performance(configs: any) {
    // trace and report perf data and pv to serve when page loaded
    // Document.readyState 属性描述了document 的加载状态。
    if (document.readyState === 'complete') {
      tracePerf.recordPerf(configs);
    } else {
      window.addEventListener(
        'load',
        () => {
          tracePerf.recordPerf(configs);
        },
        false,
      );
    }
    if (this.customOptions.enableSPA) {
      // hash router
      window.addEventListener(
        'hashchange',
        () => {
          tracePerf.recordPerf(configs);
        },
        false,
      );
    }
  },
// 性能数据对象
export type IPerfDetail = {
  redirectTime: number | undefined; // Time of redirection
  dnsTime: number | undefined; // DNS query time
  ttfbTime: number | undefined; // Time to First Byte
  tcpTime: number | undefined; // Tcp connection time
  transTime: number | undefined; // Content transfer time
  domAnalysisTime: number | undefined; // Dom parsing time
  fptTime: number | undefined; // First Paint Time or Blank Screen Time
  domReadyTime: number | undefined; // Dom ready time
  loadPageTime: number | undefined; // Page full load time
  resTime: number | undefined; // Synchronous load resources in the page
  sslTime: number | undefined; // Only valid for HTTPS
  ttlTime: number | undefined; // Time to interact
  firstPackTime: number | undefined; // first pack time
  fmpTime: number | undefined; // First Meaningful Paint
};
// fmp对象
let fmp: { fmpTime: number | undefined } = { fmpTime: undefined };

获取数据-window.performance

浏览器提供的 performance api,是性能监控数据的主要来源。performance 提供高精度的时间戳,精度可达纳秒级别,且不会随操作系统时间设置的影响。

这里也是使用该原生api。

this.perfConfig.perfDetail = await new pagePerf().getPerfTiming();
fmp = await new FMP();

getPerfTiming()方法从下面两个渠道中的一个获取性能数据对象:

const nt2Timing = performance.getEntriesByType('navigation')[0]; // 优先使用
let { timing } = window.performance as PerformanceNavigationTiming | any;

具体性能数据对象的处理方式和输出结果见附录2。

FMP()方法通过自定义算法计算出页面主要内容加载所需时间。该时间是自定义算法决定的,在性能指标中没有确定标准。

发送数据-XMLHttpRequest

输出结果 = 性能数据对象+FMP指标+版本号+服务名+网页地址

new Report('PERF', options.collector).sendByXhr(perfInfo);

在report的构造函数中利用’PERF’的信息生成url地址。

if (type === 'PERF') {
      this.url = collector + ReportTypes.PERF;
}
---
export enum ReportTypes {
  ...
  PERF = '/browser/perfData',
  ...
}

后使用XMLHttpRequest发送数据。

  public sendByXhr(data: any) {
    if (!this.url) {
      return;
    }
    const xhr = new XMLHttpRequest();

    xhr.open('post', this.url, true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4 && xhr.status < 400) {
        console.log('Report successfully');
      }
    };
    xhr.send(JSON.stringify(data));
  }

重要参数

autoTracePerf 是否上报performance性能监控

useFmp 是否开启页面主要完成时间监控。

2.2 handleErrors()

JSErrors

前端代码异常指的是以下两种情况:

  1. JS脚本里边存着语法错误;
  2. JS脚本在运行时发生错误。

有什么方法可以抓到这个错误,有两个方案:

  1. try, catch方案。你可以针对某个代码块使用try,catch包装,这个代码块运行时出错时能在catch块里边捕捉到。
  2. window.onerror方案。也可以通过window.addEventListener(“error”, function(evt){}),这个方法能捕捉到语法错误跟运行时错误,同时还能知道出错的信息,以及出错的文件,行号,列号。还可以在window.onerror最后return true让浏览器不输出错误信息到控制台。

这里就是使用该api触发错误报告。

class JSErrors extends Base {   
	window.onerror = (message, url, line, col, error) => {
      this.logInfo = {
        uniqueId: uuid(),
        service: options.service,
        serviceVersion: options.serviceVersion,
        pagePath: options.pagePath,
        category: ErrorsCategory.JS_ERROR,
        grade: GradeTypeEnum.ERROR,
        errorUrl: url,
        line,
        col,
        message,
        collector: options.collector,
        stack: error.stack,
      }; 
      this.traceInfo();
    };
}

注意,这里的this.traceInfo()继承自Base类。在这个方法中将错误信息logInfo的数据上报。

PromiseErrors

可以在代码中看到,prmomiseErrors的处理和JSErrors相同,都是继承自Base的traceInfo方法,最终将logInfo的值导出,只是错误信息的来源不同。

promiseErrors错误信息来源来自于监听事件unhandledrejection。

class PromiseErrors extends Base {
  public handleErrors(options: { service: string; serviceVersion: string; pagePath: string; collector: string }) {
    window.addEventListener('unhandledrejection', (event) => {
        ...
    })
}

当Promise 被 reject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件。

ResourceError

window.addEventListener('error', (event) => {...}))

addEventListener(‘error’)

监听js运行时错误事件,会比window.onerror先触发,与onerror的功能大体类似,不过事件回调函数传参只有一个保存所有错误信息的参数,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误。

该报错参数中包含目标资源名称。来自于错误事件的target属性。

其他错误监控类似。

VueError

Vue.config.errorHandler = (error: Error, vm: any, info: string) => {...}

Vue.config.errorHandler

对于 Vue.js 的错误上报需要用其提供的 Vue.config.errorhandler 方法,但是错误一旦被这个方法捕获,就不会外抛到在控制台

那么要怎么做才能把错误外抛到控制台呢?

简单点,就加个console.error(err)

2.3 traceSegment

export default function traceSegment(options: CustomOptionsType) {
  const segments = [] as SegmentFields[];
  // inject interceptor 注入拦截器
  xhrInterceptor(options, segments); 
  windowFetch(options, segments);
  // onbeforeunload 事件在即将离开当前页面(刷新或关闭)时触发。
  window.onbeforeunload = function (e: Event) {
    if (!segments.length) {
      return;
    }
    new Report('SEGMENTS', options.collector).sendByXhr(segments);
  };
  //report per options.traceTimeInterval min 周期性触发
  setInterval(() => {
    if (!segments.length) {
      return;
    }
    new Report('SEGMENTS', options.collector).sendByXhr(segments);
    segments.splice(0, segments.length);
  }, options.traceTimeInterval);
}

重要参数

traceTimeInterval 跟踪时间间隔最小值 number值 默认单位为毫秒

注入拦截器

  xhrInterceptor(options, segments); 
  windowFetch(options, segments);

ajax拦截

这里如果有所疑问,建议阅读本章相关知识部分。

这里首先取到了XMLHttpRequest对象,注意这里是取到了对象本身而不是new它,平时我们使用XMLHttpRequest的时候是new一个实例,而这里取到了构造函数本身,和它的构造函数send和open。

  const originalXHR = window.XMLHttpRequest as any;
  const xhrSend = XMLHttpRequest.prototype.send;
  const xhrOpen = XMLHttpRequest.prototype.open; 

originalXhr在customizedXHR方法中被修改,后将该方法赋值给原来的XMLHttpRequest对象。

(window as any).XMLHttpRequest = customizedXHR;

也就是说,customizedXHR方法实际上返回一个修改后的window.XMLHttpRequest对象。

通过这种方式,后期我们调用window.XMLHttpRequest对象进行http请求时,实际上获取的是修改后的,也就是设置了拦截器的XMLHttpRequest对象。

而后,设置xhrReadyStateChange监听事件,过滤掉不需要监控的http请求后,赋值并返回信息,这就是最终的监控信息segments数组中的一项了。

流程走完,来说说XMLHttpRequest被修改了什么。我们贴出刚刚提到的用于修改的customizedXHR方法。

这个方法如果有疑问,可以查看本章相关知识中的,this究竟做了什么。

  function customizedXHR() {
    const liveXHR = new originalXHR();

    liveXHR.addEventListener(
      'readystatechange',
      function () {
        ajaxEventTrigger.call(this, 'xhrReadyStateChange');
      },
      false,
    );

    liveXHR.open = function (
      method: string,
      url: string,
      async: boolean,
      username?: string | null,
      password?: string | null,
    ) {
      this.getRequestConfig = arguments;

      return xhrOpen.apply(this, arguments);
    };
    liveXHR.send = function (body?: Document | BodyInit | null) {
      return xhrSend.apply(this, arguments);
    };

    return liveXHR;
  }

修改了三部分,liveXHR.addEventListener、liveXHR.open、liveXHR.send。

周期性触发

这里也是将segments的信息使用Report.sendByXhr返回到后端,可以看到segments原始值是一个空数组,在拦截器中赋值,后作为参数返回到后端,我们先看看segments的类型。

export interface SegmentFields {
  traceId: string;
  service: string;
  spans: SpanFields[];
  serviceInstance: string;
  traceSegmentId: string;
}

export interface SpanFields {
  operationName: string;
  startTime: number;
  endTime: number;
  spanId: number;
  spanLayer: string;
  spanType: string;
  isError: boolean;
  parentSpanId: number;
  componentId: number;
  peer: string;
  tags?: any;
}

这些值是什么含义?我们可以在上一节注入拦截器中找答案。

相关知识

XMLHttpRequest对象

XMLHttpRequest对象提供了各种方法用于初始化和处理HTTP请求。

open()方法

调用open(DOMString method,DOMString uri,boolean async,DOMString username,DOMString password)方法初始化一个XMLHttpRequest对象。其中,method参数是必须提供的-用于指定你想用来发送请求的HTTP方法(GET,POST,PUT,DELETE或HEAD)。另外,uri参数用于指定XMLHttpRequest对象把请求发送到的服务器相应的URI。

在调用open()方法后,XMLHttpRequest对象把它的readyState属性设置为1(打开)。

send()方法

在通过调用open()方法准备好一个请求之后,你需要把该请求发送到服务器。仅当readyState值为1时,你才可以调用send()方法;否则的话,XMLHttpRequest对象将引发一个异常。该请求被使用提供给open()方法的参数发送到服务器。

在调用send()方法后,XMLHttpRequest对象把readyState的值设置为2(发送)。当服务器响应时,在接收消息体之前,如果存在任何消息体的话,XMLHttpRequest对象将把readyState设置为3(正在接收中)。当请求完成加载时,它把readyState设置为4(已加载)。

this究竟做了什么

附录1 performance对象详解

常用属性

eventCounts

memory 基本内存使用情况,Chrome 添加的一个非标准扩展

navigation 页面是加载还是刷新、发生了多少次重定向

onresourcetimingbufferfull

timeOrigin 性能测量开始时的时间的高精度时间戳

timing 页面加载的各阶段时长,给出了开始时间和结束时间的时间戳。

常用方法

performance.getEntries()

这个方法可以获取到所有的 performance 实体对象,通过 getEntriesByNamegetEntriesByType 方法可对所有的 performance 实体对象 进行过滤,返回特定类型的实体。

mark 方法 和 measure 方法的结合可打点计时,获取某个函数执行耗时等。

导航性能公式

performance.getEntriesByType(“navigation”)[0]

可以取到下面指标:不同阶段之间是不连续的,每个阶段不一定会发生

  • 重定向次数:performance.navigation.redirectCount
  • 重定向耗时: redirectEnd - redirectStart
  • DNS 解析耗时: domainLookupEnd - domainLookupStart
  • TCP 连接耗时: connectEnd - connectStart
  • SSL 安全连接耗时: connectEnd - secureConnectionStart
  • 网络请求耗时 (TTFB): responseStart - requestStart
  • 数据传输耗时: responseEnd - responseStart
  • DOM 解析耗时: domInteractive - responseEnd
  • 资源加载耗时: loadEventStart - domContentLoadedEventEnd
  • 首包时间: responseStart - domainLookupStart
  • 白屏时间: responseEnd - fetchStart
  • 首次可交互时间: domInteractive - fetchStart
  • DOM Ready 时间: domContentLoadEventEnd - fetchStart
  • 页面完全加载时间: loadEventStart - fetchStart
  • http 头部大小:transferSize - encodedBodySize

performance.getEntriesByType(“resource”) 资源加载时间

performance.getEntriesByType(“paint”) 首屏幕渲染时间

通过getEntriesByType我们可以获得具体元素的性能信息,示例如下。

let p = window.performance.getEntries();

重定向次数:performance.navigation.redirectCount

JS 资源数量: p.filter(ele => ele.initiatorType === "script").length

CSS 资源数量:p.filter(ele => ele.initiatorType === "css").length

AJAX 请求数量:p.filter(ele => ele.initiatorType === "xmlhttprequest").length

IMG 资源数量:p.filter(ele => ele.initiatorType === "img").length

总资源数量: window.performance.getEntriesByType("resource").length

性能指标公式

不重复的耗时时段区分:

  • 重定向耗时: redirectEnd - redirectStart
  • DNS 解析耗时: domainLookupEnd - domainLookupStart
  • TCP 连接耗时: connectEnd - connectStart
  • SSL 安全连接耗时: connectEnd - secureConnectionStart
  • 网络请求耗时 (TTFB): responseStart - requestStart
  • html 下载耗时:responseEnd - responseStart
  • DOM 解析耗时: domInteractive - responseEnd
  • 资源加载耗时: loadEventStart - domContentLoadedEventEnd

其他组合分析:

  • 白屏时间: domLoading - fetchStart
  • 粗略首屏时间: loadEventEnd - fetchStart 或者 domInteractive - fetchStart
  • DOM Ready 时间: domContentLoadEventEnd - fetchStart
  • 页面完全加载时间: loadEventStart - fetchStart

JS 总加载耗时:

const p = window.performance.getEntries();
let cssR = p.filter(ele => ele.initiatorType === "script");
Math.max(...cssR.map((ele) => ele.responseEnd)) - Math.min(...cssR.map((ele) => ele.startTime))

CSS 总加载耗时:

const p = window.performance.getEntries();
let cssR = p.filter(ele => ele.initiatorType === "css");
Math.max(...cssR.map((ele) => ele.responseEnd)) - Math.min(...cssR.map((ele) => ele.startTime));

打点记时示例

p e r f o r m a n c e . m a r k ( n a m e ) performance.mark(name) performance.mark(name)

根据给出 name 值,在浏览器的性能输入缓冲区中创建一个相关的时间戳
p e r f o r m a n c e . m e a s u r e ( n a m e , s t a r t M a r k , e n d M a r k ) performance.measure(name, startMark, endMark) performance.me乐维监控keycloak单点登录实操(上篇)

乐维监控keycloak单点登录实操(上篇)

前端工程实训

前端上传插件Plupload的实际使用(个人实操)

前端 监控

前端 监控