简介
Apache SkyWalking Client-side JavaScript exception and tracing library.
- Provide metrics and error collection to SkyWalking backend.
- Lightweight
- Make browser as a start of whole distributed tracing
客户端初始化
下载
npm install skywalking-client-js --save
初始化
import ClientMonitor from 'skywalking-client-js';
ClientMonitor.register({
  collector: 'http://127.0.0.1:8080',
  service: 'test-ui',
  pagePath: '/current/page/name',
  serviceVersion: 'v1.0.0',
});
源码目录

入口文件分析
该项目的入口文件为 src/index.ts 但其内部实际上只是引用了 src/monitor.ts 并将其暴露出去。
import ClientMonitor from './monitor';
(window as any).ClientMonitor = ClientMonitor;
export default ClientMonitor;
src/monitor.ts
import { CustomOptionsType, CustomReportOptions } from './types';
import { JSErrors, PromiseErrors, AjaxErrors, ResourceErrors, VueErrors, FrameErrors } from './errors/index';
import tracePerf from './performance/index';
import traceSegment from './trace/segment';
const ClientMonitor = {
 
  // ...
  register(configs: CustomOptionsType) {
    // 1. 合并 options
    this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
    // 2. 错误处理
    this.catchErrors(this.customOptions);
    // 3. performenc处理
    if (!this.customOptions.enableSPA) {
      this.performance(this.customOptions);
    }
    // 4. api 监控
    traceSegment(this.customOptions);
  },
  performance(configs: any) {
    // trace and report perf data and pv to serve when page loaded
    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,
      );
    }
  },
  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 });
    }
  },
  setPerformance(configs: CustomOptionsType) {
    // history router
    this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
    this.performance(this.customOptions);
  },
  reportFrameErrors(configs: CustomReportOptions, error: Error) {
    FrameErrors.handleErrors(configs, error);
  },
};
export default ClientMonitor;
错误监控
src/monitor.ts
// ...
import { JSErrors, PromiseErrors, AjaxErrors, ResourceErrors, VueErrors, FrameErrors } from './errors/index';
// ...
register(configs: CustomOptionsType) {
    this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
    this.catchErrors(this.customOptions);
  	
    // ...
},
  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 });
    }
  },
JSErrors
src/errors/js.ts
// ...
class JSErrors extends Base {
  public handleErrors(options: { service: string; serviceVersion: string; pagePath: string; collector: string }) {
    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();
    };
  }
}
export default new JSErrors();
以上通过 window.onerror 监听 js 报错。
traceInfo 方法实现的逻辑如下:
public traceInfo() {
  // ...
	
  // 1. 记录错误
  this.handleRecordError();
  // 2. 发送错误
  setTimeout(() => {
    Task.fireTasks();
  }, 100);
}
- 记录错误
private handleRecordError() {
  try {
    if (!this.logInfo.message) {
      return;
    }
    const errorInfo = this.handleErrorInfo();
    
    // 1.1 Task 示例添加错误信息
    Task.addTask(errorInfo);
  } catch (error) {
    throw error;
  }
}
private handleErrorInfo() {
    let message = `error category:${this.logInfo.category}\r\n log info:${this.logInfo.message}\r\n
      error url: ${this.logInfo.errorUrl}\r\n `;
    switch (this.logInfo.category) {
      case ErrorsCategory.JS_ERROR:
        message += `error line number: ${this.logInfo.line}\r\n error col number:${this.logInfo.col}\r\n`;
        break;
      default:
        message;
        break;
    }
    const recordInfo = {
      ...this.logInfo,
      message,
    };
    return recordInfo;
}
- 发送错误
上报及收集信息由 src/services/task.ts 完成

Report 类内部封装了 fetch 及 xmlHttpRequest 两种上报方式,一下不再赘述。
VueErrors
src/errors/vue.ts
vue 内部的错误都被 catch 到了 ,window.onerror 无法监控到, 需要使用 Vue.config.errorHandler  捕获到错误并发送错误信息。
// ...
class VueErrors extends Base {
  public handleErrors(
    options: { service: string; pagePath: string; serviceVersion: string; collector: string },
    Vue: any,
  ) {
    Vue.config.errorHandler = (error: Error, vm: any, info: string) => {
      try {
        this.logInfo = {
          uniqueId: uuid(),
          service: options.service,
          serviceVersion: options.serviceVersion,
          pagePath: options.pagePath,
          category: ErrorsCategory.VUE_ERROR,
          grade: GradeTypeEnum.ERROR,
          errorUrl: location.href,
          message: info,
          collector: options.collector,
          stack: error.stack,
        };
        this.traceInfo();
      } catch (error) {
        throw error;
      }
    };
  }
}
export default new VueErrors();
PromiseErrors
function forgetCatchError () {
  async()
    .then(() => {
      // code..
    })
    .then(() => console.log('forget catch error!'));
}
上面的示例代码中 async() 中和后续的两个 then 中的代码如果出错或者 reject ,错误没有得到处理。在没有使用 catch 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。当promise被 reject 并且错误信息没有被处理的时候,会抛出 unhandledrejection,这个错误不会被 window.onerror 和 addEventListener(“error”) 所监听到。
src/errors/promise.ts
// ...
class PromiseErrors extends Base {
  public handleErrors(options: { service: string; serviceVersion: string; pagePath: string; collector: string }) {
    window.addEventListener('unhandledrejection', (event) => {
      try {
        // ...
        
        this.logInfo = {
          // ...
        };
        this.traceInfo();
      } catch (error) {
        console.log(error);
      }
    });
  }
}
export default new PromiseErrors();
AjaxErrors
src/errors/ajax.ts
// ...
// 1. 原生的 send 方法
const xhrSend = XMLHttpRequest.prototype.send;
const xhrEvent = (event: any) => {
  try {
    if (event && event.currentTarget && (event.currentTarget.status >= 400 || event.currentTarget.status === 0)) {
      this.logInfo = {
       // ...
      };
      this.traceInfo();
    }
  } catch (error) {
    console.log(error);
  }
};
// 2. 重写 XMLHttpRequest 中 send 方法, 添加内部自定义事件并调用远程的 send 方法
XMLHttpRequest.prototype.send = function () {
  if (this.addEventListener) {
    this.addEventListener('error', xhrEvent);
    this.addEventListener('abort', xhrEvent);
    this.addEventListener('timeout', xhrEvent);
  } else {
    const stateChange = this.onreadystatechange;
    this.onreadystatechange = function (event: any) {
      stateChange.apply(this, arguments);
      if (this.readyState === 4) {
        // 3. 重新调用原生的 send 方法
        xhrEvent(event);
      }
    };
  }
  return xhrSend.apply(this, arguments);
};
// ...
ResourceErrors
src/errors/resource.ts
// ....
// 1.
window.addEventListener('error', (event) => {
  try {
    if (!event) {
      return;
    }
    const target: any = event.target || event.srcElement;
    const isElementTarget =
          target instanceof HTMLScriptElement ||
          target instanceof HTMLLinkElement ||
          target instanceof HTMLImageElement;
    if (!isElementTarget) {
      // return js error
      return;
    }
    this.logInfo = {
      uniqueId: uuid(),
      service: options.service,
      serviceVersion: options.serviceVersion,
      pagePath: options.pagePath,
      category: ErrorsCategory.RESOURCE_ERROR,
      grade: target.tagName === 'IMG' ? GradeTypeEnum.WARNING : GradeTypeEnum.ERROR,
      errorUrl: target.src || target.href || location.href,
      message: `load ${target.tagName} resource error`,
      collector: options.collector,
      stack: `load ${target.tagName} resource error`,
    };
    this.traceInfo();
  } catch (error) {
    throw error;
  }
});
window.onerror 与 window.addEventListener(‘error’, function(event) { … }) 区别
- window.onerror
window.onerror是一个全局变量,默认值为null。当有js运行时错误触发时,window会触发error事件,并执行window.onerror()。onerror可以接受多个参数。
- message:错误信息(字符串)。可用于HTML onerror=”“处理程序中的event。
- source:发生错误的脚本URL(字符串)
- lineno:发生错误的行号(数字)
- colno:发生错误的列号(数字)
- error:Error对象
若该函数返回true,则阻止执行默认事件处理函数,如异常信息不会在console中打印。
没有返回值或者返回值为false的时候,异常信息会在console中打印。
- window.addEventListener
 
监听js运行时错误事件,会比window.onerror先触发,与onerror的功能大体类似,不过事件回调函数传参只有一个保存所有错误信息的参数,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误。
当资源(如img或script)加载失败,加载资源的元素会触发一个Event接口的error事件,并执行该元素上的onerror()处理函数。这些error事件不会向上冒泡到window,但可以在捕获阶段被捕获 因此如果要全局监听资源加载错误,需要在捕获阶段捕获事件。
api 监控
src/monitor.ts
// ...
import traceSegment from './trace/segment';
// ...
register(configs: CustomOptionsType) {
    this.customOptions = {
      ...this.customOptions,
      ...configs,
    };
  
    // ...
    traceSegment(this.customOptions);
}
// ...
src/trace/segment.ts 
 
上面的代码中引入  interceptors/xhr.ts 及  interceptors/fetch.ts 文件对 xmlhttprequest 及 fetch 的监控。
interceptors/xhr.ts
重写 原生 XMLHttpRequest

上面的代码主要做了 XMLHttpRequest 做了二次封装,并将 window 的原始 XMLHttpRequest 方法进行了重新赋值。其中内部的 customizedXHR 方法中:

上述方法中监听了 XMLHttpRequest  readystatechange 事件,并触发了通过 ` const ajaxEvent = new CustomEvent(event, { detail: this })` 定义的自定义事件 xhrReadyStateChange。
其中将涉及到的参数存入到 getRequestConfig 中:

以上就是为了能暂存请求参数及配置。
触发 xhrReadyStateChange 事件
1.初始化对象及处理 url

2.判断当前接口是否在不跟踪的接口列表内

3.排除不需要上报的接口地址以及追踪开关是否开启

4.当 XMLHttpRequest 中 readyState 为 1 时,即表示已调用send()方法正在向服务端发送请求,此时生成 traceId 、接口开始时间、数据并设置请求头、segCollector 数组增加一个新的对象如下:

5.当  XMLHttpRequest  中  readyState 为 4 时 表示请求完成, 遍历 segCollector 如果中的对象满足 segCollector[i].event.readyState === 4 构建 exitSpan 对象并存储到 segent 对象上并在 segCollector  剔除掉。
上报收集到的消息
1.监听浏览器  onbeforeunload  事件后统一将收集到的信息上报上去。

2.定时发送

Performance
src/monitor.ts
performance(configs: any) {
  // trace and report perf data and pv to serve when page loaded
  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,
    );
  }
}
document.readyState
0-UNINITIALIZED:XML 对象被产生,但没有任何文件被加载。
1-LOADING:加载程序进行中,但文件尚未开始解析。
2-LOADED:部分的文件已经加载且进行解析,但对象模型尚未生效。
3-INTERACTIVE:仅对已加载的部分文件有效,在此情况下,对象模型是有效但只读的。
4-COMPLETED:文件已完全加载,代表加载成功。
window.addEventListener(‘load’, () => {})
- html 文档中的图片资源,js 代码中有异步加载的 css、js 、图片资源都加载完毕之后
window.onload = function() { } 传统事件只能执行一次
window.addEventListener(‘load’ , function() { })
- HTML 文档被完全加载和解析完成,无需等待样式表、图像和子框架的完成加载之后
document.addEventListener(‘DOMContentLoaded’,funtion( ) { } )  与jQuery的 ready( ) 方法同理。
$( document ).ready(function() {
    console.log( "ready!" );
});
参考链接
src/performance/perf.ts
```javascript
// …
const { timing } = window.performance; let redirectTime = 0;
if (timing.navigationStart !== undefined) { redirectTime = parseInt(String(timing.fetchStart - timing.navigationStart), 10); } else if (timing.redirectEnd !== undefined) { redirectTime = parseInt(String(timing.redirectEnd - timing.redirectStart), 10); } else { redirectTime = 0; }
- return {
- // 重定向时间
- redirectTime,
- // DNS解析时间
- // DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长
- // 可使用 HTML5 Prefetch 预查询 DNS ,见:HTML5 prefetch
- dnsTime: parseInt(String(timing.domainLookupEnd - timing.domainLookupStart), 10),
- // 读取页面第一个字节的时间
- // 可以理解为用户拿到你的资源占用的时间
- ttfbTime: parseInt(String(timing.responseStart - timing.requestStart), 10), // Time to First Byte
- // TCP完成握手时间
- tcpTime: parseInt(String(timing.connectEnd - timing.connectStart), 10),
- // HTTP请求响应完成时间
- transTime: parseInt(String(timing.responseEnd - timing.responseStart), 10),
- // DOM结构解析完成时间
- domAnalysisTime: parseInt(String(timing.domInteractive - timing.responseEnd), 10),
- // 首次刷新或白屏时间
- fptTime: parseInt(String(timing.responseEnd - timing.fetchStart), 10), // First Paint Time or Blank Screen Time
- // domready时间
- domReadyTime: parseInt(String(timing.domContentLoadedEventEnd - timing.fetchStart), 10),
- // 页面完整加载时间
- // 这几乎代表了用户等待页面可用的时间
- loadPageTime: parseInt(String(timing.loadEventStart - timing.fetchStart), 10), // Page full load time
- // 资源请求耗时
- resTime: parseInt(String(timing.loadEventStart - timing.domContentLoadedEventEnd), 10),
- // TLS的验证时间(https)
- sslTime:
- location.protocol === ‘https:’ && timing.secureConnectionStart > 0
- ? parseInt(String(timing.connectEnd - timing.secureConnectionStart), 10)
- undefined, // 用户可交互事件 ttlTime: parseInt(String(timing.domInteractive - timing.fetchStart), 10), // time to interact // 第一个包的时间 firstPackTime: parseInt(String(timing.responseStart - timing.domainLookupStart), 10), // first pack time fmpTime: 0, // First Meaningful Paint };
// … ```