diff --git a/packages/browser-utils/src/instrument/xhr.ts b/packages/browser-utils/src/instrument/xhr.ts index e97b7e54be60..3563f30cba19 100644 --- a/packages/browser-utils/src/instrument/xhr.ts +++ b/packages/browser-utils/src/instrument/xhr.ts @@ -15,14 +15,17 @@ type WindowWithXhr = Window & { XMLHttpRequest?: typeof XMLHttpRequest }; * Use at your own risk, this might break without changelog notice, only used internally. * @hidden */ -export function addXhrInstrumentationHandler(handler: (data: HandlerDataXhr) => void): void { +export function addXhrInstrumentationHandler( + handler: (data: HandlerDataXhr) => void, + httpClientInstrumented?: boolean, +): void { const type = 'xhr'; addHandler(type, handler); - maybeInstrument(type, instrumentXHR); + maybeInstrument(type, () => instrumentXHR(httpClientInstrumented)); } /** Exported only for tests. */ -export function instrumentXHR(): void { +export function instrumentXHR(httpClientInstrumented: boolean = false): void { if (!(WINDOW as WindowWithXhr).XMLHttpRequest) { return; } @@ -32,6 +35,13 @@ export function instrumentXHR(): void { // eslint-disable-next-line @typescript-eslint/unbound-method xhrproto.open = new Proxy(xhrproto.open, { apply(originalOpen, xhrOpenThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, xhrOpenArgArray) { + // NOTE: If you are a Sentry user, and you are seeing this stack frame, + // it means the error, that was caused by your XHR call did not + // have a stack trace. If you are using HttpClient integration, + // this is the expected behavior, as we are using this virtual error to capture + // the location of your XHR call, and group your HttpClient events accordingly. + const virtualError = new Error(); + const startTimestamp = timestampInSeconds() * 1000; // open() should always be called with two or more arguments @@ -75,6 +85,7 @@ export function instrumentXHR(): void { endTimestamp: timestampInSeconds() * 1000, startTimestamp, xhr: xhrOpenThisArg, + error: httpClientInstrumented ? virtualError : undefined, }; triggerHandlers('xhr', handlerData); } diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index 439941c97faf..c7097b77d30c 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -46,7 +46,7 @@ const _httpClientIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, - setup(client): void { + setup(client: Client): void { _wrapFetch(client, _options); _wrapXHR(client, _options); }, @@ -70,6 +70,7 @@ function _fetchResponseHandler( requestInfo: RequestInfo, response: Response, requestInit?: RequestInit, + error?: unknown, ): void { if (_shouldCaptureResponse(options, response.status, response.url)) { const request = _getRequest(requestInfo, requestInit); @@ -89,9 +90,13 @@ function _fetchResponseHandler( responseHeaders, requestCookies, responseCookies, + stacktrace: error instanceof Error ? error.stack : undefined, }); + // withScope(scope => { + // scope.setFingerprint([request.url, request.method, response.status.toString()]); captureEvent(event); + // }); } } @@ -151,6 +156,9 @@ function _xhrResponseHandler( requestHeaders = headers; } + const virtualError = new Error(); + const virtualStacktrace = virtualError.stack; + const event = _createEvent({ url: xhr.responseURL, method, @@ -159,6 +167,7 @@ function _xhrResponseHandler( // Can't access request cookies from XHR responseHeaders, responseCookies, + stacktrace: virtualStacktrace, }); captureEvent(event); @@ -283,20 +292,24 @@ function _wrapFetch(client: Client, options: HttpClientOptions): void { return; } - addFetchInstrumentationHandler(handlerData => { - if (getClient() !== client) { - return; - } + addFetchInstrumentationHandler( + handlerData => { + if (getClient() !== client) { + return; + } - const { response, args } = handlerData; - const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; + const { response, args } = handlerData; + const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; - if (!response) { - return; - } + if (!response) { + return; + } - _fetchResponseHandler(options, requestInfo, response as Response, requestInit); - }); + _fetchResponseHandler(options, requestInfo, response as Response, requestInit, handlerData.error); + }, + false, + true, + ); } /** @@ -327,7 +340,7 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void { } catch (e) { DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); } - }); + }, true); } /** @@ -358,7 +371,15 @@ function _createEvent(data: { responseCookies?: Record; requestHeaders?: Record; requestCookies?: Record; + stacktrace?: string; }): SentryEvent { + const client = getClient(); + const virtualStackTrace = client && data.stacktrace ? data.stacktrace : undefined; + const stack = virtualStackTrace && client ? client.getOptions().stackParser(virtualStackTrace) : undefined; + + // Remove the first frame from the stack as it's the HttpClient call + const nonSentryStack = stack && stack.length ? stack.slice(1) : undefined; + const message = `HTTP Client Error with status code: ${data.status}`; const event: SentryEvent = { @@ -368,6 +389,7 @@ function _createEvent(data: { { type: 'Error', value: message, + stacktrace: stack ? { frames: nonSentryStack } : undefined, }, ], }, diff --git a/packages/core/src/utils-hoist/instrument/fetch.ts b/packages/core/src/utils-hoist/instrument/fetch.ts index ad28edf81e3f..cb06b8c12a8e 100644 --- a/packages/core/src/utils-hoist/instrument/fetch.ts +++ b/packages/core/src/utils-hoist/instrument/fetch.ts @@ -21,10 +21,11 @@ type FetchResource = string | { toString(): string } | { url: string }; export function addFetchInstrumentationHandler( handler: (data: HandlerDataFetch) => void, skipNativeFetchCheck?: boolean, + httpClientInstrumented?: boolean, ): void { const type = 'fetch'; addHandler(type, handler); - maybeInstrument(type, () => instrumentFetch(undefined, skipNativeFetchCheck)); + maybeInstrument(type, () => instrumentFetch(undefined, skipNativeFetchCheck, httpClientInstrumented)); } /** @@ -41,7 +42,11 @@ export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFet maybeInstrument(type, () => instrumentFetch(streamHandler)); } -function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNativeFetchCheck: boolean = false): void { +function instrumentFetch( + onFetchResolved?: (response: Response) => void, + skipNativeFetchCheck: boolean = false, + httpClientInstrumented: boolean = false, +): void { if (skipNativeFetchCheck && !supportsNativeFetch()) { return; } @@ -59,7 +64,9 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat }; // if there is no callback, fetch is instrumented directly - if (!onFetchResolved) { + // if httpClientInstrumented is true, we are in the HttpClient instrumentation + // and we may need to capture the stacktrace even when the fetch promise is resolved + if (!onFetchResolved && !httpClientInstrumented) { triggerHandlers('fetch', { ...handlerData, }); @@ -72,7 +79,8 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat // it means the error, that was caused by your fetch call did not // have a stack trace, so the SDK backfilled the stack trace so // you can see which fetch call failed. - const virtualStackTrace = new Error().stack; + const virtualError = new Error(); + const virtualStackTrace = virtualError.stack; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return originalFetch.apply(GLOBAL_OBJ, args).then( @@ -80,10 +88,12 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat if (onFetchResolved) { onFetchResolved(response); } else { + // Adding the stacktrace to be able to fingerprint the failed fetch event in HttpClient instrumentation triggerHandlers('fetch', { ...handlerData, endTimestamp: timestampInSeconds() * 1000, response, + error: httpClientInstrumented ? virtualError : undefined, }); } diff --git a/packages/types/src/instrument.ts b/packages/types/src/instrument.ts index f0b239e86b14..f472a663c5d5 100644 --- a/packages/types/src/instrument.ts +++ b/packages/types/src/instrument.ts @@ -32,6 +32,7 @@ export interface HandlerDataXhr { xhr: SentryWrappedXMLHttpRequest; startTimestamp?: number; endTimestamp?: number; + error?: unknown; } interface SentryFetchData {