Skip to content

Commit 2cbf64e

Browse files
authored
ref(browser): Use Proxy for XHR instrumentation (#12212)
1 parent eb1c108 commit 2cbf64e

File tree

1 file changed

+42
-36
lines changed
  • packages/browser-utils/src/instrument

1 file changed

+42
-36
lines changed

packages/browser-utils/src/instrument/xhr.ts

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, WrappedFunction } from '@sentry/types';
1+
import type { HandlerDataXhr, SentryWrappedXMLHttpRequest } from '@sentry/types';
22

3-
import { addHandler, fill, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils';
3+
import { addHandler, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils';
44
import { WINDOW } from '../types';
55

66
export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__';
@@ -29,108 +29,114 @@ export function instrumentXHR(): void {
2929

3030
const xhrproto = XMLHttpRequest.prototype;
3131

32-
fill(xhrproto, 'open', function (originalOpen: () => void): () => void {
33-
return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: unknown[]): void {
32+
// eslint-disable-next-line @typescript-eslint/unbound-method
33+
xhrproto.open = new Proxy(xhrproto.open, {
34+
apply(originalOpen, xhrOpenThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, xhrOpenArgArray) {
3435
const startTimestamp = timestampInSeconds() * 1000;
3536

3637
// open() should always be called with two or more arguments
3738
// But to be on the safe side, we actually validate this and bail out if we don't have a method & url
38-
const method = isString(args[0]) ? args[0].toUpperCase() : undefined;
39-
const url = parseUrl(args[1]);
39+
const method = isString(xhrOpenArgArray[0]) ? xhrOpenArgArray[0].toUpperCase() : undefined;
40+
const url = parseUrl(xhrOpenArgArray[1]);
4041

4142
if (!method || !url) {
42-
return originalOpen.apply(this, args);
43+
return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray);
4344
}
4445

45-
this[SENTRY_XHR_DATA_KEY] = {
46+
xhrOpenThisArg[SENTRY_XHR_DATA_KEY] = {
4647
method,
4748
url,
4849
request_headers: {},
4950
};
5051

5152
// if Sentry key appears in URL, don't capture it as a request
5253
if (method === 'POST' && url.match(/sentry_key/)) {
53-
this.__sentry_own_request__ = true;
54+
xhrOpenThisArg.__sentry_own_request__ = true;
5455
}
5556

5657
const onreadystatechangeHandler: () => void = () => {
5758
// For whatever reason, this is not the same instance here as from the outer method
58-
const xhrInfo = this[SENTRY_XHR_DATA_KEY];
59+
const xhrInfo = xhrOpenThisArg[SENTRY_XHR_DATA_KEY];
5960

6061
if (!xhrInfo) {
6162
return;
6263
}
6364

64-
if (this.readyState === 4) {
65+
if (xhrOpenThisArg.readyState === 4) {
6566
try {
6667
// touching statusCode in some platforms throws
6768
// an exception
68-
xhrInfo.status_code = this.status;
69+
xhrInfo.status_code = xhrOpenThisArg.status;
6970
} catch (e) {
7071
/* do nothing */
7172
}
7273

7374
const handlerData: HandlerDataXhr = {
7475
endTimestamp: timestampInSeconds() * 1000,
7576
startTimestamp,
76-
xhr: this,
77+
xhr: xhrOpenThisArg,
7778
};
7879
triggerHandlers('xhr', handlerData);
7980
}
8081
};
8182

82-
if ('onreadystatechange' in this && typeof this.onreadystatechange === 'function') {
83-
fill(this, 'onreadystatechange', function (original: WrappedFunction) {
84-
return function (this: SentryWrappedXMLHttpRequest, ...readyStateArgs: unknown[]): void {
83+
if ('onreadystatechange' in xhrOpenThisArg && typeof xhrOpenThisArg.onreadystatechange === 'function') {
84+
xhrOpenThisArg.onreadystatechange = new Proxy(xhrOpenThisArg.onreadystatechange, {
85+
apply(originalOnreadystatechange, onreadystatechangeThisArg, onreadystatechangeArgArray: unknown[]) {
8586
onreadystatechangeHandler();
86-
return original.apply(this, readyStateArgs);
87-
};
87+
return originalOnreadystatechange.apply(onreadystatechangeThisArg, onreadystatechangeArgArray);
88+
},
8889
});
8990
} else {
90-
this.addEventListener('readystatechange', onreadystatechangeHandler);
91+
xhrOpenThisArg.addEventListener('readystatechange', onreadystatechangeHandler);
9192
}
9293

9394
// Intercepting `setRequestHeader` to access the request headers of XHR instance.
9495
// This will only work for user/library defined headers, not for the default/browser-assigned headers.
9596
// Request cookies are also unavailable for XHR, as `Cookie` header can't be defined by `setRequestHeader`.
96-
fill(this, 'setRequestHeader', function (original: WrappedFunction) {
97-
return function (this: SentryWrappedXMLHttpRequest, ...setRequestHeaderArgs: unknown[]): void {
98-
const [header, value] = setRequestHeaderArgs;
97+
xhrOpenThisArg.setRequestHeader = new Proxy(xhrOpenThisArg.setRequestHeader, {
98+
apply(
99+
originalSetRequestHeader,
100+
setRequestHeaderThisArg: SentryWrappedXMLHttpRequest,
101+
setRequestHeaderArgArray: unknown[],
102+
) {
103+
const [header, value] = setRequestHeaderArgArray;
99104

100-
const xhrInfo = this[SENTRY_XHR_DATA_KEY];
105+
const xhrInfo = setRequestHeaderThisArg[SENTRY_XHR_DATA_KEY];
101106

102107
if (xhrInfo && isString(header) && isString(value)) {
103108
xhrInfo.request_headers[header.toLowerCase()] = value;
104109
}
105110

106-
return original.apply(this, setRequestHeaderArgs);
107-
};
111+
return originalSetRequestHeader.apply(setRequestHeaderThisArg, setRequestHeaderArgArray);
112+
},
108113
});
109114

110-
return originalOpen.apply(this, args);
111-
};
115+
return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray);
116+
},
112117
});
113118

114-
fill(xhrproto, 'send', function (originalSend: () => void): () => void {
115-
return function (this: XMLHttpRequest & SentryWrappedXMLHttpRequest, ...args: unknown[]): void {
116-
const sentryXhrData = this[SENTRY_XHR_DATA_KEY];
119+
// eslint-disable-next-line @typescript-eslint/unbound-method
120+
xhrproto.send = new Proxy(xhrproto.send, {
121+
apply(originalSend, sendThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, sendArgArray: unknown[]) {
122+
const sentryXhrData = sendThisArg[SENTRY_XHR_DATA_KEY];
117123

118124
if (!sentryXhrData) {
119-
return originalSend.apply(this, args);
125+
return originalSend.apply(sendThisArg, sendArgArray);
120126
}
121127

122-
if (args[0] !== undefined) {
123-
sentryXhrData.body = args[0];
128+
if (sendArgArray[0] !== undefined) {
129+
sentryXhrData.body = sendArgArray[0];
124130
}
125131

126132
const handlerData: HandlerDataXhr = {
127133
startTimestamp: timestampInSeconds() * 1000,
128-
xhr: this,
134+
xhr: sendThisArg,
129135
};
130136
triggerHandlers('xhr', handlerData);
131137

132-
return originalSend.apply(this, args);
133-
};
138+
return originalSend.apply(sendThisArg, sendArgArray);
139+
},
134140
});
135141
}
136142

0 commit comments

Comments
 (0)