|
1 |
| -import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, WrappedFunction } from '@sentry/types'; |
| 1 | +import type { HandlerDataXhr, SentryWrappedXMLHttpRequest } from '@sentry/types'; |
2 | 2 |
|
3 |
| -import { addHandler, fill, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils'; |
| 3 | +import { addHandler, isString, maybeInstrument, timestampInSeconds, triggerHandlers } from '@sentry/utils'; |
4 | 4 | import { WINDOW } from '../types';
|
5 | 5 |
|
6 | 6 | export const SENTRY_XHR_DATA_KEY = '__sentry_xhr_v3__';
|
@@ -29,108 +29,114 @@ export function instrumentXHR(): void {
|
29 | 29 |
|
30 | 30 | const xhrproto = XMLHttpRequest.prototype;
|
31 | 31 |
|
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) { |
34 | 35 | const startTimestamp = timestampInSeconds() * 1000;
|
35 | 36 |
|
36 | 37 | // open() should always be called with two or more arguments
|
37 | 38 | // 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]); |
40 | 41 |
|
41 | 42 | if (!method || !url) {
|
42 |
| - return originalOpen.apply(this, args); |
| 43 | + return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray); |
43 | 44 | }
|
44 | 45 |
|
45 |
| - this[SENTRY_XHR_DATA_KEY] = { |
| 46 | + xhrOpenThisArg[SENTRY_XHR_DATA_KEY] = { |
46 | 47 | method,
|
47 | 48 | url,
|
48 | 49 | request_headers: {},
|
49 | 50 | };
|
50 | 51 |
|
51 | 52 | // if Sentry key appears in URL, don't capture it as a request
|
52 | 53 | if (method === 'POST' && url.match(/sentry_key/)) {
|
53 |
| - this.__sentry_own_request__ = true; |
| 54 | + xhrOpenThisArg.__sentry_own_request__ = true; |
54 | 55 | }
|
55 | 56 |
|
56 | 57 | const onreadystatechangeHandler: () => void = () => {
|
57 | 58 | // 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]; |
59 | 60 |
|
60 | 61 | if (!xhrInfo) {
|
61 | 62 | return;
|
62 | 63 | }
|
63 | 64 |
|
64 |
| - if (this.readyState === 4) { |
| 65 | + if (xhrOpenThisArg.readyState === 4) { |
65 | 66 | try {
|
66 | 67 | // touching statusCode in some platforms throws
|
67 | 68 | // an exception
|
68 |
| - xhrInfo.status_code = this.status; |
| 69 | + xhrInfo.status_code = xhrOpenThisArg.status; |
69 | 70 | } catch (e) {
|
70 | 71 | /* do nothing */
|
71 | 72 | }
|
72 | 73 |
|
73 | 74 | const handlerData: HandlerDataXhr = {
|
74 | 75 | endTimestamp: timestampInSeconds() * 1000,
|
75 | 76 | startTimestamp,
|
76 |
| - xhr: this, |
| 77 | + xhr: xhrOpenThisArg, |
77 | 78 | };
|
78 | 79 | triggerHandlers('xhr', handlerData);
|
79 | 80 | }
|
80 | 81 | };
|
81 | 82 |
|
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[]) { |
85 | 86 | onreadystatechangeHandler();
|
86 |
| - return original.apply(this, readyStateArgs); |
87 |
| - }; |
| 87 | + return originalOnreadystatechange.apply(onreadystatechangeThisArg, onreadystatechangeArgArray); |
| 88 | + }, |
88 | 89 | });
|
89 | 90 | } else {
|
90 |
| - this.addEventListener('readystatechange', onreadystatechangeHandler); |
| 91 | + xhrOpenThisArg.addEventListener('readystatechange', onreadystatechangeHandler); |
91 | 92 | }
|
92 | 93 |
|
93 | 94 | // Intercepting `setRequestHeader` to access the request headers of XHR instance.
|
94 | 95 | // This will only work for user/library defined headers, not for the default/browser-assigned headers.
|
95 | 96 | // 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; |
99 | 104 |
|
100 |
| - const xhrInfo = this[SENTRY_XHR_DATA_KEY]; |
| 105 | + const xhrInfo = setRequestHeaderThisArg[SENTRY_XHR_DATA_KEY]; |
101 | 106 |
|
102 | 107 | if (xhrInfo && isString(header) && isString(value)) {
|
103 | 108 | xhrInfo.request_headers[header.toLowerCase()] = value;
|
104 | 109 | }
|
105 | 110 |
|
106 |
| - return original.apply(this, setRequestHeaderArgs); |
107 |
| - }; |
| 111 | + return originalSetRequestHeader.apply(setRequestHeaderThisArg, setRequestHeaderArgArray); |
| 112 | + }, |
108 | 113 | });
|
109 | 114 |
|
110 |
| - return originalOpen.apply(this, args); |
111 |
| - }; |
| 115 | + return originalOpen.apply(xhrOpenThisArg, xhrOpenArgArray); |
| 116 | + }, |
112 | 117 | });
|
113 | 118 |
|
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]; |
117 | 123 |
|
118 | 124 | if (!sentryXhrData) {
|
119 |
| - return originalSend.apply(this, args); |
| 125 | + return originalSend.apply(sendThisArg, sendArgArray); |
120 | 126 | }
|
121 | 127 |
|
122 |
| - if (args[0] !== undefined) { |
123 |
| - sentryXhrData.body = args[0]; |
| 128 | + if (sendArgArray[0] !== undefined) { |
| 129 | + sentryXhrData.body = sendArgArray[0]; |
124 | 130 | }
|
125 | 131 |
|
126 | 132 | const handlerData: HandlerDataXhr = {
|
127 | 133 | startTimestamp: timestampInSeconds() * 1000,
|
128 |
| - xhr: this, |
| 134 | + xhr: sendThisArg, |
129 | 135 | };
|
130 | 136 | triggerHandlers('xhr', handlerData);
|
131 | 137 |
|
132 |
| - return originalSend.apply(this, args); |
133 |
| - }; |
| 138 | + return originalSend.apply(sendThisArg, sendArgArray); |
| 139 | + }, |
134 | 140 | });
|
135 | 141 | }
|
136 | 142 |
|
|
0 commit comments