diff --git a/CHANGELOG.md b/CHANGELOG.md index c38172ad72..e53e934d6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Prevent pod install crash when visionos is not present ([#3548](https://github.com/getsentry/sentry-react-native/pull/3548)) - Fetch Organization slug from `@sentry/react-native/expo` config when uploading artifacts ([#3557](https://github.com/getsentry/sentry-react-native/pull/3557)) +- Remove 404 Http Client Errors reports for Metro Dev Server Requests ([#3553](https://github.com/getsentry/sentry-react-native/pull/3553)) ## 5.17.0 diff --git a/src/js/integrations/debugsymbolicator.ts b/src/js/integrations/debugsymbolicator.ts index e449c605b6..970042f214 100644 --- a/src/js/integrations/debugsymbolicator.ts +++ b/src/js/integrations/debugsymbolicator.ts @@ -3,6 +3,7 @@ import { addContextToFrame, logger } from '@sentry/utils'; import { getFramesToPop, isErrorLike } from '../utils/error'; import { ReactNativeLibraries } from '../utils/rnlibraries'; +import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr'; import type * as ReactNative from '../vendor/react-native'; const INTERNAL_CALLSITES_REGEX = new RegExp(['ReactNativeRenderer-dev\\.js$', 'MessageQueue\\.js$'].join('|')); @@ -200,14 +201,30 @@ export class DebugSymbolicator implements Integration { * Get source context for segment */ private async _fetchSourceContext(url: string, segments: Array, start: number): Promise { - const response = await fetch(`${url}${segments.slice(start).join('/')}`, { - method: 'GET', - }); + return new Promise(resolve => { + const fullUrl = `${url}${segments.slice(start).join('/')}`; - if (response.ok) { - return response.text(); - } - return null; + const xhr = createStealthXhr(); + if (!xhr) { + resolve(null); + return; + } + + xhr.open('GET', fullUrl, true); + xhr.send(); + + xhr.onreadystatechange = (): void => { + if (xhr.readyState === XHR_READYSTATE_DONE) { + if (xhr.status !== 200) { + resolve(null); + } + resolve(xhr.responseText); + } + }; + xhr.onerror = (): void => { + resolve(null); + }; + }); } /** diff --git a/src/js/utils/worldwide.ts b/src/js/utils/worldwide.ts index 7c4e2ea4ec..debd6da3ab 100644 --- a/src/js/utils/worldwide.ts +++ b/src/js/utils/worldwide.ts @@ -16,6 +16,7 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { nativeFabricUIManager: unknown; ErrorUtils?: ErrorUtils; expo?: ExpoGlobalObject; + XMLHttpRequest?: typeof XMLHttpRequest; } /** Get's the global object for the current JavaScript runtime */ diff --git a/src/js/utils/xhr.ts b/src/js/utils/xhr.ts new file mode 100644 index 0000000000..c64d5a7a31 --- /dev/null +++ b/src/js/utils/xhr.ts @@ -0,0 +1,41 @@ +import { RN_GLOBAL_OBJ } from './worldwide'; + +const __sentry_original__ = '__sentry_original__'; + +type XMLHttpRequestWithSentryOriginal = XMLHttpRequest & { + open: typeof XMLHttpRequest.prototype.open & { [__sentry_original__]?: typeof XMLHttpRequest.prototype.open }; + send: typeof XMLHttpRequest.prototype.send & { [__sentry_original__]?: typeof XMLHttpRequest.prototype.send }; +}; + +/** + * The DONE ready state for XmlHttpRequest + * + * Defining it here as a constant b/c XMLHttpRequest.DONE is not always defined + * (e.g. during testing, it is `undefined`) + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState} + */ +export const XHR_READYSTATE_DONE = 4; + +/** + * Creates a new XMLHttpRequest object which is not instrumented by the SDK. + * + * This request won't be captured by the HttpClient Errors integration + * and won't be added to breadcrumbs and won't be traced. + */ +export function createStealthXhr( + customGlobal: { XMLHttpRequest?: typeof XMLHttpRequest } = RN_GLOBAL_OBJ, +): XMLHttpRequest | null { + if (!customGlobal.XMLHttpRequest) { + return null; + } + + const xhr: XMLHttpRequestWithSentryOriginal = new customGlobal.XMLHttpRequest(); + if (xhr.open.__sentry_original__) { + xhr.open = xhr.open.__sentry_original__; + } + if (xhr.send.__sentry_original__) { + xhr.send = xhr.send.__sentry_original__; + } + return xhr; +} diff --git a/test/utils/xhr.test.ts b/test/utils/xhr.test.ts new file mode 100644 index 0000000000..2e9b16fc2a --- /dev/null +++ b/test/utils/xhr.test.ts @@ -0,0 +1,76 @@ +import { createStealthXhr } from '../../src/js/utils/xhr'; + +describe('xhr', () => { + it('creates xhr and calls monkey patched methods if original was not preserved', () => { + const XMLHttpRequestMock = getXhrMock(); + const globalMock = createGlobalMock(XMLHttpRequestMock); + + const xhr = createStealthXhr(globalMock); + + xhr!.open('GET', 'https://example.com'); + xhr!.send(); + + expect(xhr!.open).toHaveBeenCalledWith('GET', 'https://example.com'); + expect(xhr!.send).toHaveBeenCalled(); + }); + + it('monkey patched xhr is not called when original is preserved', () => { + const XMLHttpRequestMock = getXhrMock(); + const globalMock = createGlobalMock(XMLHttpRequestMock); + + const { xhrOpenMonkeyPatch, xhrSendMonkeyPatch } = mockSentryPatchWithOriginal(globalMock); + + const xhr = createStealthXhr(globalMock); + + xhr!.open('GET', 'https://example.com'); + xhr!.send(); + + expect(xhrOpenMonkeyPatch).not.toHaveBeenCalled(); + expect(xhrSendMonkeyPatch).not.toHaveBeenCalled(); + expect(xhr!.open).toHaveBeenCalledWith('GET', 'https://example.com'); + expect(xhr!.send).toHaveBeenCalled(); + }); +}); + +function createGlobalMock(xhr: unknown) { + return { + XMLHttpRequest: xhr as typeof XMLHttpRequest, + }; +} + +function getXhrMock() { + function XhrMock() {} + + XhrMock.prototype.open = jest.fn(); + XhrMock.prototype.send = jest.fn(); + + return XhrMock; +} + +type WithSentryOriginal = T & { __sentry_original__?: T }; + +function mockSentryPatchWithOriginal(globalMock: { XMLHttpRequest: typeof XMLHttpRequest }): { + xhrOpenMonkeyPatch: jest.Mock; + xhrSendMonkeyPatch: jest.Mock; +} { + const originalOpen = globalMock.XMLHttpRequest.prototype.open; + const originalSend = globalMock.XMLHttpRequest.prototype.send; + + const xhrOpenMonkeyPatch = jest.fn(); + const xhrSendMonkeyPatch = jest.fn(); + + globalMock.XMLHttpRequest.prototype.open = xhrOpenMonkeyPatch; + globalMock.XMLHttpRequest.prototype.send = xhrSendMonkeyPatch; + + ( + globalMock.XMLHttpRequest.prototype.open as WithSentryOriginal + ).__sentry_original__ = originalOpen; + ( + globalMock.XMLHttpRequest.prototype.send as WithSentryOriginal + ).__sentry_original__ = originalSend; + + return { + xhrOpenMonkeyPatch, + xhrSendMonkeyPatch, + }; +}