前言
前置关于addInstrumentationHandler
和fill
方法可以在第一篇文章中了解sentry-javascript解析(一)fetch如何捕获
前置准备
我们首先重新复习一下如何使用XHR
发送一个请求。
// 来源mdn
const req = new XMLHttpRequest();
req.addEventListener("load", (res) => console.log(res));
req.open("GET", "http://www.example.org/example.txt");
req.send();
接下来我们看看sentry
是如何捕获XHR
的。
XHR错误捕获
指定url捕获
这里与fetch
方法捕获是共用的方法,在sentry
初始化的时候,我们可以通过tracingOrigins
捕获哪些url
,sentry
通过作用域闭包缓存所有应该捕获的url
,省去重复的遍历。
// 作用域闭包
const urlMap: Record<string, boolean> = {};
// 用于判断当前url是否应该被捕获
const defaultShouldCreateSpan = (url: string): boolean => {
if (urlMap[url]) {
return urlMap[url];
}
const origins = tracingOrigins;
// 缓存url省去重复遍历
urlMap[url] =
origins.some((origin: string | RegExp) => isMatchingPattern(url, origin)) &&
!isMatchingPattern(url, 'sentry_key');
return urlMap[url];
};
添加捕获回调
接下来,我们在@sentry/browser
中看到:
if (traceXHR) {
addInstrumentationHandler({
callback: (handlerData: XHRData) => {
xhrCallback(handlerData, shouldCreateSpan, spans);
},
type: 'xhr',
});
}
高阶函数封装XHR
按照addInstrumentationHandler
的代码我们可以准确看出通过type: 'xhr'
接下来应该执行instrumentXHR
方法,我们来看一下这个方法的代码:
function instrumentXHR() {
if (!('XMLHttpRequest' in global)) {
return;
}
const requestKeys: XMLHttpRequest[] = [];
const requestValues: Array<any>[] = [];
const xhrproto = XMLHttpRequest.prototype;
// 封装XHR的open方法
fill(
xhrproto,
'open',
function(originalOpen: () => void) {
return function(this: SentryWrappedXMLHttpRequest, ...args: any[]) {
const xhr = this;
const url = args[1];
// 缓存本次请求的method和url
xhr.__sentry_xhr__ = {
method: isString(args[0]) ? args[0].toUpperCase() : args[0],
url: args[1],
};
if (isString(url) && xhr.__sentry_xhr__.method === 'POST' && url.match(/sentry_key/)) {
// 如果是post请求,且请求地址中包含了sentry_key字样,则添加__sentry_own_request__标志此次请求为sentry上报发出的
xhr.__sentry_own_request__ = true;
}
// readyState变化回调
const onreadystatechangeHandler = function(): void {
// 4表示请求结束
if (xhr.readyState === 4) {
try {
if (xhr.__sentry_xhr__) {
// 记录响应状态
xhr.__sentry_xhr__.status_code = xhr.status;
}
} catch (e) {
}
try {
const requestPos = requestKeys.indexOf(xhr);
if (requestPos !== -1) {
// 弹出send时缓存的请求内容
requestKeys.splice(requestPos);
const args = requestValues.splice(requestPos)[0];
if (xhr.__sentry_xhr__ && args[0] !== undefined) {
xhr.__sentry_xhr__.body = args[0] as XHRSendInput;
}
}
} catch (e) {
/* do nothing */
}
// 遍历xhr对应回调
triggerHandlers('xhr', {
args,
endTimestamp: Date.now(),
startTimestamp: Date.now(),
xhr,
});
}
};
if ('onreadystatechange' in xhr && typeof xhr.onreadystatechange === 'function') {
// 如果onreadystatechange是一个方法,则使用高阶函数封装onreadystatechange方法
fill(xhr, 'onreadystatechange', function(original: WrappedFunction): Function {
return function(...readyStateArgs: any[]): void {
onreadystatechangeHandler();
return original.apply(xhr, readyStateArgs);
};
});
} else {
// 否则直接监听onreadystatechange事件
xhr.addEventListener('readystatechange', onreadystatechangeHandler);
}
// 原生方法调用
return originalOpen.apply(xhr, args);
};
});
// 封装XHR的send方法
fill(xhrproto, 'send', function(originalSend: () => void): () => void {
return function(this: SentryWrappedXMLHttpRequest, ...args: any[]): void {
// 缓存本次请求的request和请求参数
requestKeys.push(this);
requestValues.push(args);
// 遍历xhr对应的回调
triggerHandlers('xhr', {
args,
startTimestamp: Date.now(),
xhr: this,
});
// 原生方法调用
return originalSend.apply(this, args);
};
});
}
我们可以通过上面的代码了解到,sentry
封装了XMLHttpRequest
的open
、send
方法,而且在用户调用open
方法时会封装onreadystatechange
方法。
捕获回调函数内都做了什么
接下来我们再看一下XHR
回调中都做了哪些事情
function xhrCallback(
handlerData: XHRData, // 拼接后的数据
shouldCreateSpan: (url: string) => boolean, // 用于判断当前url是否应该被捕获
spans: Record<string, Span>, // 全局缓存事务
): void {
// 获取用户当前的配置
const currentClientOptions = getCurrentHub().getClient()?.getOptions();
if (
!(currentClientOptions && hasTracingEnabled(currentClientOptions)) ||
!(handlerData.xhr && handlerData.xhr.__sentry_xhr__ && shouldCreateSpan(handlerData.xhr.__sentry_xhr__.url)) ||
handlerData.xhr.__sentry_own_request__
) {
return;
}
// 获取在open方法时记录的method和url
const xhr = handlerData.xhr.__sentry_xhr__;
if (handlerData.endTimestamp && handlerData.xhr.__sentry_xhr_span_id__) {
// 请求结束
const span = spans[handlerData.xhr.__sentry_xhr_span_id__];
if (span) {
// 记录响应状态码
span.setHttpStatus(xhr.status_code);
span.finish();
delete spans[handlerData.xhr.__sentry_xhr_span_id__];
}
return;
}
// 创建一个新的事务
const activeTransaction = getActiveTransaction();
if (activeTransaction) {
const span = activeTransaction.startChild({
data: {
...xhr.data,
type: 'xhr',
method: xhr.method,
url: xhr.url,
},
description: `${xhr.method} ${xhr.url}`,
op: 'http',
});
// 添加事物唯一标志
handlerData.xhr.__sentry_xhr_span_id__ = span.spanId;
spans[handlerData.xhr.__sentry_xhr_span_id__] = span;
if (handlerData.xhr.setRequestHeader) {
try {
// xhr请求时,在请求头添加sentry-trace字段
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
} catch (_) {
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
}
}
}
}
通过上面的代码,我们可以了解到在发送请求的时候,sentry
会通过setRequestHeader
方法添加sentry-trace
请求头。在请求结束后,上报本次请求相关信息。
总结
对比sentry
对fetch
的封装,我们可以发现两者大部分还是神似的,我们按照步骤总结一下:
- 由用户配置
traceXHR
确认开启XHR
捕获,配置tracingOrigins
确认要捕获的url
- 通过
shouldCreateSpanForRequest
添加对XHR
的声明周期的回调- 内部调用
instrumentXHR
对全局的XHR
做二次封装- 封装
open
、send
方法,其中在调用open
方法时会封装onreadystatechange
方法/事件
- 封装
- 内部调用
- 用户调用
XHR
的open
方法- 缓存本次请求的
method
、url
- 封装
onreadystatechange
方法/事件 - 调用原生
open
方法
- 缓存本次请求的
- 用户调用
XHR
的send
方法- 遍历上一步添加的回调函数
- 创建唯一事务用于上报信息
- 在请求头中添加
sentry-trace
字段
- 调用原生
send
方法
- 遍历上一步添加的回调函数
onreadystatechange
状态改变触发回调- 如果当前状态为
4
请求结束,记录请求状态码 - 遍历上一步添加的回调函数
- 上报本次请求
- 如果当前状态为
- 结束本次捕获
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!