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()
- 参数在setPerformance中处理后,传入performance函数。
- performance函数对情况进行判断,在合适的时机触发处理函数。
- 先判断页面是否加载完成,是则触发处理函数,否则添加 load监听事件,触发处理函数。
- 然后判断是否进行单页面应用监控,是则添加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()
- performance函数对情况进行判断,在合适的时机触发处理函数。
- 先判断页面是否加载完成,是则触发处理函数,否则添加load监听事件,触发函数,获取数据。
- 然后判断是否进行单页面应用监控,是则添加hashchange监听事件,触发函数,获取数据。
- 发送性能数据到服务端
处理-整合性能数据
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
前端代码异常指的是以下两种情况:
- JS脚本里边存着语法错误;
- JS脚本在运行时发生错误。
有什么方法可以抓到这个错误,有两个方案:
- try, catch方案。你可以针对某个代码块使用try,catch包装,这个代码块运行时出错时能在catch块里边捕捉到。
- 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
实体对象,通过 getEntriesByName
和 getEntriesByType
方法可对所有的 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单点登录实操(上篇)