Skip to content

Commit 4e41557

Browse files
fix(debugsymbolicator): Don't trace debug symbolicator source context Metro Dev Server requests (#3553)
1 parent a418c83 commit 4e41557

File tree

5 files changed

+143
-7
lines changed

5 files changed

+143
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
- Prevent pod install crash when visionos is not present ([#3548](https://github.com/getsentry/sentry-react-native/pull/3548))
2525
- Fetch Organization slug from `@sentry/react-native/expo` config when uploading artifacts ([#3557](https://github.com/getsentry/sentry-react-native/pull/3557))
26+
- Remove 404 Http Client Errors reports for Metro Dev Server Requests ([#3553](https://github.com/getsentry/sentry-react-native/pull/3553))
2627

2728
## 5.17.0
2829

src/js/integrations/debugsymbolicator.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { addContextToFrame, logger } from '@sentry/utils';
33

44
import { getFramesToPop, isErrorLike } from '../utils/error';
55
import { ReactNativeLibraries } from '../utils/rnlibraries';
6+
import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr';
67
import type * as ReactNative from '../vendor/react-native';
78

89
const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|'));
@@ -200,14 +201,30 @@ export class DebugSymbolicator implements Integration {
200201
* Get source context for segment
201202
*/
202203
private async _fetchSourceContext(url: string, segments: Array<string>, start: number): Promise<string | null> {
203-
const response = await fetch(`${url}${segments.slice(start).join('/')}`, {
204-
method: 'GET',
205-
});
204+
return new Promise(resolve => {
205+
const fullUrl = `${url}${segments.slice(start).join('/')}`;
206206

207-
if (response.ok) {
208-
return response.text();
209-
}
210-
return null;
207+
const xhr = createStealthXhr();
208+
if (!xhr) {
209+
resolve(null);
210+
return;
211+
}
212+
213+
xhr.open('GET', fullUrl, true);
214+
xhr.send();
215+
216+
xhr.onreadystatechange = (): void => {
217+
if (xhr.readyState === XHR_READYSTATE_DONE) {
218+
if (xhr.status !== 200) {
219+
resolve(null);
220+
}
221+
resolve(xhr.responseText);
222+
}
223+
};
224+
xhr.onerror = (): void => {
225+
resolve(null);
226+
};
227+
});
211228
}
212229

213230
/**

src/js/utils/worldwide.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
1616
nativeFabricUIManager: unknown;
1717
ErrorUtils?: ErrorUtils;
1818
expo?: ExpoGlobalObject;
19+
XMLHttpRequest?: typeof XMLHttpRequest;
1920
}
2021

2122
/** Get's the global object for the current JavaScript runtime */

src/js/utils/xhr.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { RN_GLOBAL_OBJ } from './worldwide';
2+
3+
const __sentry_original__ = '__sentry_original__';
4+
5+
type XMLHttpRequestWithSentryOriginal = XMLHttpRequest & {
6+
open: typeof XMLHttpRequest.prototype.open & { [__sentry_original__]?: typeof XMLHttpRequest.prototype.open };
7+
send: typeof XMLHttpRequest.prototype.send & { [__sentry_original__]?: typeof XMLHttpRequest.prototype.send };
8+
};
9+
10+
/**
11+
* The DONE ready state for XmlHttpRequest
12+
*
13+
* Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined
14+
* (e.g. during testing, it is `undefined`)
15+
*
16+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState}
17+
*/
18+
export const XHR_READYSTATE_DONE = 4;
19+
20+
/**
21+
* Creates a new XMLHttpRequest object which is not instrumented by the SDK.
22+
*
23+
* This request won't be captured by the HttpClient Errors integration
24+
* and won't be added to breadcrumbs and won't be traced.
25+
*/
26+
export function createStealthXhr(
27+
customGlobal: { XMLHttpRequest?: typeof XMLHttpRequest } = RN_GLOBAL_OBJ,
28+
): XMLHttpRequest | null {
29+
if (!customGlobal.XMLHttpRequest) {
30+
return null;
31+
}
32+
33+
const xhr: XMLHttpRequestWithSentryOriginal = new customGlobal.XMLHttpRequest();
34+
if (xhr.open.__sentry_original__) {
35+
xhr.open = xhr.open.__sentry_original__;
36+
}
37+
if (xhr.send.__sentry_original__) {
38+
xhr.send = xhr.send.__sentry_original__;
39+
}
40+
return xhr;
41+
}

test/utils/xhr.test.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { createStealthXhr } from '../../src/js/utils/xhr';
2+
3+
describe('xhr', () => {
4+
it('creates xhr and calls monkey patched methods if original was not preserved', () => {
5+
const XMLHttpRequestMock = getXhrMock();
6+
const globalMock = createGlobalMock(XMLHttpRequestMock);
7+
8+
const xhr = createStealthXhr(globalMock);
9+
10+
xhr!.open('GET', 'https://example.com');
11+
xhr!.send();
12+
13+
expect(xhr!.open).toHaveBeenCalledWith('GET', 'https://example.com');
14+
expect(xhr!.send).toHaveBeenCalled();
15+
});
16+
17+
it('monkey patched xhr is not called when original is preserved', () => {
18+
const XMLHttpRequestMock = getXhrMock();
19+
const globalMock = createGlobalMock(XMLHttpRequestMock);
20+
21+
const { xhrOpenMonkeyPatch, xhrSendMonkeyPatch } = mockSentryPatchWithOriginal(globalMock);
22+
23+
const xhr = createStealthXhr(globalMock);
24+
25+
xhr!.open('GET', 'https://example.com');
26+
xhr!.send();
27+
28+
expect(xhrOpenMonkeyPatch).not.toHaveBeenCalled();
29+
expect(xhrSendMonkeyPatch).not.toHaveBeenCalled();
30+
expect(xhr!.open).toHaveBeenCalledWith('GET', 'https://example.com');
31+
expect(xhr!.send).toHaveBeenCalled();
32+
});
33+
});
34+
35+
function createGlobalMock(xhr: unknown) {
36+
return {
37+
XMLHttpRequest: xhr as typeof XMLHttpRequest,
38+
};
39+
}
40+
41+
function getXhrMock() {
42+
function XhrMock() {}
43+
44+
XhrMock.prototype.open = jest.fn();
45+
XhrMock.prototype.send = jest.fn();
46+
47+
return XhrMock;
48+
}
49+
50+
type WithSentryOriginal<T> = T & { __sentry_original__?: T };
51+
52+
function mockSentryPatchWithOriginal(globalMock: { XMLHttpRequest: typeof XMLHttpRequest }): {
53+
xhrOpenMonkeyPatch: jest.Mock;
54+
xhrSendMonkeyPatch: jest.Mock;
55+
} {
56+
const originalOpen = globalMock.XMLHttpRequest.prototype.open;
57+
const originalSend = globalMock.XMLHttpRequest.prototype.send;
58+
59+
const xhrOpenMonkeyPatch = jest.fn();
60+
const xhrSendMonkeyPatch = jest.fn();
61+
62+
globalMock.XMLHttpRequest.prototype.open = xhrOpenMonkeyPatch;
63+
globalMock.XMLHttpRequest.prototype.send = xhrSendMonkeyPatch;
64+
65+
(
66+
globalMock.XMLHttpRequest.prototype.open as WithSentryOriginal<typeof XMLHttpRequest.prototype.open>
67+
).__sentry_original__ = originalOpen;
68+
(
69+
globalMock.XMLHttpRequest.prototype.send as WithSentryOriginal<typeof XMLHttpRequest.prototype.send>
70+
).__sentry_original__ = originalSend;
71+
72+
return {
73+
xhrOpenMonkeyPatch,
74+
xhrSendMonkeyPatch,
75+
};
76+
}

0 commit comments

Comments
 (0)