简介
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 };
// … ```