本文基于skywalking-client-js源码做前端监控实现分析,建议下载其源码对比观看。
源码代码分为入口和功能代码两部分,入口提供不同的调用方式,功能代码是通过入口代码调用的。
入口文件位于skywalking-client-jssrc/monitor.ts
功能代码位于src中skywalking-client-js/errors/performance/services/traces四个文件夹下。
ClientMonitor下面有五个方法,可以通过调用这些方法来使用client.js。
给出默认配置对象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)
状态的事件即将被触发。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时开启性能监控。
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
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 };
浏览器提供的 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()方法通过自定义算法计算出页面主要内容加载所需时间。该时间是自定义算法决定的,在性能指标中没有确定标准。
输出结果 = 性能数据对象+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 是否开启页面主要完成时间监控。
前端代码异常指的是以下两种情况:
有什么方法可以抓到这个错误,有两个方案:
这里就是使用该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的数据上报。
可以在代码中看到,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 事件。
window.addEventListener('error', (event) => {
...}))
addEventListener(‘error’)
监听js运行时错误事件,会比window.onerror先触发,与onerror的功能大体类似,不过事件回调函数传参只有一个保存所有错误信息的参数,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误。
该报错参数中包含目标资源名称。来自于错误事件的target属性。
其他错误监控类似。
Vue.config.errorHandler = (error: Error, vm: any, info: string) => {
...}
Vue.config.errorHandler
对于 Vue.js 的错误上报需要用其提供的 Vue.config.errorhandler
方法,但是错误一旦被这个方法捕获,就不会外抛到在控制台。
那么要怎么做才能把错误外抛到控制台呢?
简单点,就加个console.error(err)
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);
这里如果有所疑问,建议阅读本章相关知识部分。
这里首先取到了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对象提供了各种方法用于初始化和处理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(已加载)。
eventCounts
memory 基本内存使用情况,Chrome 添加的一个非标准扩展
navigation 页面是加载还是刷新、发生了多少次重定向
onresourcetimingbufferfull
timeOrigin 性能测量开始时的时间的高精度时间戳
timing 页面加载的各阶段时长,给出了开始时间和结束时间的时间戳。
performance.getEntries()
这个方法可以获取到所有的 performance
实体对象,通过 getEntriesByName
和 getEntriesByType
方法可对所有的 performance
实体对象 进行过滤,返回特定类型的实体。
mark 方法 和 measure 方法的结合可打点计时,获取某个函数执行耗时等。
performance.getEntriesByType(“navigation”)[0]
可以取到下面指标:不同阶段之间是不连续的,每个阶段不一定会发生
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
不重复的耗时时段区分:
其他组合分析:
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.measure(name,startMark,endMark)
这里接收三个参数:
name:测量的名字
startMark:测量的开始标志名字(也可以是 PerformanceTiming 属性的名称)
endMark:测量的结束标志名字(也可以是 PerformanceTiming 属性的名称)
// 标记开始
performance.mark("myTimestart");
// ...
dosometing();
// 标记结束
performance.mark("myTimeend");
// 标记开始点和结束点之间的时间戳
performance.measure(
"myTime",
"myTimestart",
"myTimeend"
);
// 获取所有名称为myTime的measures
var measures = performance.getEntriesByName("myTime");
var measure = measures[0];
console.log("myTime milliseconds:", measure.duration);
// 清除标记
performance.clearMarks();
performance.clearMeasures();
注:timing = performance.getEntriesByType(“navigation”)[0];
上面代码在附录1 常用方法 导航性能公式 一节解析。
下面来源中的属性和含义可以对照附录1中的常用属性一节。
名称 | 来源 | 含义 |
---|---|---|
redirectTime | 自定义代码 | Time of redirection |
dnsTime | timing.domainLookupEnd - timing.domainLookupStart | DNS query time |
ttfbTime | timing.responseStart - timing.requestStarTime to First Byte | Time to First Byte |
tcpTime | timing.connectEnd - timing.connectStart | Tcp connection time |
transTime | timing.responseEnd - timing.responseStart | Content transfer time |
domAnalysisTime | timing.domInteractive - timing.responseEnd | Dom parsing time |
fptTime | timing.responseEnd - timing.fetchStart | First Paint Time or Blank Screen Time |
domReadyTime | timing.domContentLoadedEventEnd - timing.fetchStart | Dom ready time |
loadPageTime | timing.loadEventStart - timing.fetchStart | Page full load time |
resTime | timing.loadEventStart - timing.domContentLoadedEventEnd | Synchronous load resources in the page |
sslTime | timing.domInteractive - timing.fetchStart | Only valid for HTTPS |
ttlTime | timing.domInteractive - timing.fetchStart | Time to interact |
firstPackTime | timing.responseStart - timing.domainLookupStart | first pack time |
fmpTime | 自定义代码 | First Meaningful Paint |
Start | Page full load time |
| resTime | timing.loadEventStart - timing.domContentLoadedEventEnd | Synchronous load resources in the page |
| sslTime | timing.domInteractive - timing.fetchStart | Only valid for HTTPS |
| ttlTime | timing.domInteractive - timing.fetchStart | Time to interact |
| firstPackTime | timing.responseStart - timing.domainLookupStart | first pack time |
| fmpTime | 自定义代码 | First Meaningful Paint |