前言
上一篇我们分享了如何进行前端的错误采集,今天我们来分析一下如何进行前端接口的采集。
数据结构
typescript
export interface IApiContext {
version: string, // 组件版本号
globalSessionId: string, // 全局会话标识
uniqueVisitorId: string, // 独立访客标识
systemName: string, // 系统简称
serviceName: string, // 服务名称
referer?: string, // 引用
userAgent: string, // 浏览器用户代理(浏览器标识)
ip?: string, // ip地址
generateTime: string | number, // 发生时间
url: string, // 页面地址
domain: string, // 页面域名
page:string,
title: string, // 页面标题
reqURL: string, // 请求地址
reqParam?: string, // 请求参数
reqMsg?: string, // 请求报文
reqTime: string | number,// 请求时间戳
httpStatusCode?:string | number,
respCode?: string, // 响应吗
respDesc?: string, // 响应描述
respMsg?: string, // 响应报文
respTime?: string | number // 响应时间戳
}
如何实现采集
-
重写xhr的Open方法、Send方法
-
监听loadend事件
这些操作都是在原型上来完成的,其实这样操作我觉得类似切面(AOP),我门能在这里拦截所有的请求,能获取到所有请求的状态码,参数,计算请求耗时,响应时间-请求时间(当然这个耗时仅供参考),要想获取准确的耗时需要使用浏览器的performance里面的resource中的耗时,这里不在做过多描述,我们在其他文章里已经全面说过这个耗时。
同时我们为了前后端日志串联,在我们拦截的所有请求中添加自定义请求头,这个请求头的值是一个唯一的uuid,这样根据这个uuid可以把前后端日志进行串联,实现全端的链路追踪。
js
export const ajaxInterceptorHandler = (options: IParams) => {
try {
if (!XMLHttpRequest) {
return;
}
let originalXhrProto = XMLHttpRequest.prototype;
let reqUrl: string = "";
let xhrInstance = null;
// @ts-ignore
// 可以拿到请求信息
replaceOld(originalXhrProto, EVENTTYPES.OPEN, (originalOpen: voidFun): voidFun => {
return function (this: BTRACEXMLHttpReques, ...args: any[]): void {
xhrInstance = this;
xhrInstance.btrace = {
url: args[1],
reqId: uuid(),
sendTime: getTimeStamp()
};
reqUrl = reqUrlTool(args[1]);
originalOpen.apply(this, args);
}
})
replaceOld(originalXhrProto, EVENTTYPES.SEND, (originalSend: voidFun): voidFun => {
return function (this: BTRACEXMLHttpReques, ...args: any[]): void {
isCludeUrl(this.btrace.url, options) && xhrInstance.setRequestHeader(HEADERS.XB3SESSIONId, getCookie(HEADERS.XB3SESSIONId));
on(this, EVENTTYPES.LOADEND, function (this: BTRACEXMLHttpReques){
if (options.apiRequestIsEnable) {
if (isCludeUrl(this.btrace.url, options) && !this.btrace.timeout) {
this.btrace.resTime = getTimeStamp();
this.btrace.url = reqUrlTool(this.btrace.url);
this.btrace.duration = this.btrace.resTime - this.btrace.sendTime;
this.btrace.reqParam = getReqParams(this.btrace.url);
createApiMsgData(xhrInstance, xhrInstance, options, ERROR_TYPE.AJAX);
}
}
});
on(this, EVENTTYPES.TIMEOUT, function (this: BTRACEXMLHttpReques){
this.btrace.resTime = getTimeStamp();
if (this && this.type === EVENTTYPES.TIMEOUT) {
this.btrace.timeout = true;
this.btrace.url = reqUrl;
this.btrace.duration = this.btrace.resTime - this.btrace.sendTime;
this.btrace.reqParam = getReqParams(this.btrace.url);
createApiMsgData(xhrInstance, xhrInstance, options, ERROR_TYPE.AJAX);
}
})
originalSend.apply(this, args);
}
})
} catch (e) {
console.warn("btrace:" + e);
}
}
- 重写fetch请求
js
export const fetchInterceptorHandler = (options: IParams) => {
try {
if (!_global.fetch) {
return;
}
replaceOld(_global, EVENTTYPES.FETCH, (originalFetch: voidFun) => {
return function (url: string, config: Partial<RequestInit> = {}): void {
let sendTime = getTimeStamp(); // 发送时间
let reqId = uuid();
const headers = config.headers || {};
if (isCludeUrl(url, options)) {
if (config && 'headers' in config) {
headers[HEADERS.XB3SESSIONId] = getCookie(HEADERS.XB3SESSIONId);
} else {
let customHeader = {
headers: {
[HEADERS.XB3SESSIONId]: getCookie(HEADERS.XB3SESSIONId)
}
};
config.headers = customHeader.headers;
}
}
return originalFetch.apply(_global, [url, config]).then(
(res: Response) => {
let resTime = getTimeStamp();
let duration = resTime - sendTime;
const tempRes = res.clone();
tempRes["btrace"] = {
resTime: resTime,
sendTime: sendTime,
duration: duration,
reqParam: getReqParams(url),
reqId: reqId
};
if (options.apiRequestIsEnable) {
createApiMsgData(tempRes, tempRes, options, ERROR_TYPE.FETCH)
}
return res;
},
(e: Error) => {
if (options.apiRequestIsEnable) {
if (isCludeUrl(url, options)) {
e["status"] = 0;
let resTime = getTimeStamp();
let duration = resTime - sendTime;
e["btrace"] = {
resTime: resTime,
sendTime: sendTime,
duration: duration,
reqParam: getReqParams(url),
reqId: reqId
};
createApiMsgData(e, e, options, ERROR_TYPE.FETCH)
}
}
throw e;
}
)
}
})
} catch (e) {
console.log('ee', e);
}
}
总结
基于以上方式我们完成了对请求的拦截并添加了我们自己的需求,在这里获取请求状态码、计算请求耗时、请求的唯一标识(这个标识能起大作用)。我们也完成了线上环境的验证,目前正在推广使用中。