Skip to content

Commit 252e544

Browse files
committed
feat(browser): Attach virtual stack traces to HttpClient events.
1 parent 8f6dd04 commit 252e544

File tree

4 files changed

+64
-20
lines changed

4 files changed

+64
-20
lines changed

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,17 @@ type WindowWithXhr = Window & { XMLHttpRequest?: typeof XMLHttpRequest };
1515
* Use at your own risk, this might break without changelog notice, only used internally.
1616
* @hidden
1717
*/
18-
export function addXhrInstrumentationHandler(handler: (data: HandlerDataXhr) => void): void {
18+
export function addXhrInstrumentationHandler(
19+
handler: (data: HandlerDataXhr) => void,
20+
httpClientInstrumented?: boolean,
21+
): void {
1922
const type = 'xhr';
2023
addHandler(type, handler);
21-
maybeInstrument(type, instrumentXHR);
24+
maybeInstrument(type, () => instrumentXHR(httpClientInstrumented));
2225
}
2326

2427
/** Exported only for tests. */
25-
export function instrumentXHR(): void {
28+
export function instrumentXHR(httpClientInstrumented: boolean = false): void {
2629
if (!(WINDOW as WindowWithXhr).XMLHttpRequest) {
2730
return;
2831
}
@@ -32,6 +35,13 @@ export function instrumentXHR(): void {
3235
// eslint-disable-next-line @typescript-eslint/unbound-method
3336
xhrproto.open = new Proxy(xhrproto.open, {
3437
apply(originalOpen, xhrOpenThisArg: XMLHttpRequest & SentryWrappedXMLHttpRequest, xhrOpenArgArray) {
38+
// NOTE: If you are a Sentry user, and you are seeing this stack frame,
39+
// it means the error, that was caused by your XHR call did not
40+
// have a stack trace. If you are using HttpClient integration,
41+
// this is the expected behavior, as we are using this virtual error to capture
42+
// the location of your XHR call, and group your HttpClient events accordingly.
43+
const virtualError = new Error();
44+
3545
const startTimestamp = timestampInSeconds() * 1000;
3646

3747
// open() should always be called with two or more arguments
@@ -75,6 +85,7 @@ export function instrumentXHR(): void {
7585
endTimestamp: timestampInSeconds() * 1000,
7686
startTimestamp,
7787
xhr: xhrOpenThisArg,
88+
error: httpClientInstrumented ? virtualError : undefined,
7889
};
7990
triggerHandlers('xhr', handlerData);
8091
}

packages/browser/src/integrations/httpclient.ts

+35-13
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const _httpClientIntegration = ((options: Partial<HttpClientOptions> = {}) => {
4646

4747
return {
4848
name: INTEGRATION_NAME,
49-
setup(client): void {
49+
setup(client: Client): void {
5050
_wrapFetch(client, _options);
5151
_wrapXHR(client, _options);
5252
},
@@ -70,6 +70,7 @@ function _fetchResponseHandler(
7070
requestInfo: RequestInfo,
7171
response: Response,
7272
requestInit?: RequestInit,
73+
error?: unknown,
7374
): void {
7475
if (_shouldCaptureResponse(options, response.status, response.url)) {
7576
const request = _getRequest(requestInfo, requestInit);
@@ -89,9 +90,13 @@ function _fetchResponseHandler(
8990
responseHeaders,
9091
requestCookies,
9192
responseCookies,
93+
stacktrace: error instanceof Error ? error.stack : undefined,
9294
});
9395

96+
// withScope(scope => {
97+
// scope.setFingerprint([request.url, request.method, response.status.toString()]);
9498
captureEvent(event);
99+
// });
95100
}
96101
}
97102

@@ -151,6 +156,9 @@ function _xhrResponseHandler(
151156
requestHeaders = headers;
152157
}
153158

159+
const virtualError = new Error();
160+
const virtualStacktrace = virtualError.stack;
161+
154162
const event = _createEvent({
155163
url: xhr.responseURL,
156164
method,
@@ -159,6 +167,7 @@ function _xhrResponseHandler(
159167
// Can't access request cookies from XHR
160168
responseHeaders,
161169
responseCookies,
170+
stacktrace: virtualStacktrace,
162171
});
163172

164173
captureEvent(event);
@@ -283,20 +292,24 @@ function _wrapFetch(client: Client, options: HttpClientOptions): void {
283292
return;
284293
}
285294

286-
addFetchInstrumentationHandler(handlerData => {
287-
if (getClient() !== client) {
288-
return;
289-
}
295+
addFetchInstrumentationHandler(
296+
handlerData => {
297+
if (getClient() !== client) {
298+
return;
299+
}
290300

291-
const { response, args } = handlerData;
292-
const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined];
301+
const { response, args } = handlerData;
302+
const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined];
293303

294-
if (!response) {
295-
return;
296-
}
304+
if (!response) {
305+
return;
306+
}
297307

298-
_fetchResponseHandler(options, requestInfo, response as Response, requestInit);
299-
});
308+
_fetchResponseHandler(options, requestInfo, response as Response, requestInit, handlerData.error);
309+
},
310+
false,
311+
true,
312+
);
300313
}
301314

302315
/**
@@ -327,7 +340,7 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void {
327340
} catch (e) {
328341
DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e);
329342
}
330-
});
343+
}, true);
331344
}
332345

333346
/**
@@ -358,7 +371,15 @@ function _createEvent(data: {
358371
responseCookies?: Record<string, string>;
359372
requestHeaders?: Record<string, string>;
360373
requestCookies?: Record<string, string>;
374+
stacktrace?: string;
361375
}): SentryEvent {
376+
const client = getClient();
377+
const virtualStackTrace = client && data.stacktrace ? data.stacktrace : undefined;
378+
const stack = virtualStackTrace && client ? client.getOptions().stackParser(virtualStackTrace) : undefined;
379+
380+
// Remove the first frame from the stack as it's the HttpClient call
381+
const nonSentryStack = stack && stack.length ? stack.slice(1) : undefined;
382+
362383
const message = `HTTP Client Error with status code: ${data.status}`;
363384

364385
const event: SentryEvent = {
@@ -368,6 +389,7 @@ function _createEvent(data: {
368389
{
369390
type: 'Error',
370391
value: message,
392+
stacktrace: stack ? { frames: nonSentryStack } : undefined,
371393
},
372394
],
373395
},

packages/core/src/utils-hoist/instrument/fetch.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ type FetchResource = string | { toString(): string } | { url: string };
2121
export function addFetchInstrumentationHandler(
2222
handler: (data: HandlerDataFetch) => void,
2323
skipNativeFetchCheck?: boolean,
24+
httpClientInstrumented?: boolean,
2425
): void {
2526
const type = 'fetch';
2627
addHandler(type, handler);
27-
maybeInstrument(type, () => instrumentFetch(undefined, skipNativeFetchCheck));
28+
maybeInstrument(type, () => instrumentFetch(undefined, skipNativeFetchCheck, httpClientInstrumented));
2829
}
2930

3031
/**
@@ -41,7 +42,11 @@ export function addFetchEndInstrumentationHandler(handler: (data: HandlerDataFet
4142
maybeInstrument(type, () => instrumentFetch(streamHandler));
4243
}
4344

44-
function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNativeFetchCheck: boolean = false): void {
45+
function instrumentFetch(
46+
onFetchResolved?: (response: Response) => void,
47+
skipNativeFetchCheck: boolean = false,
48+
httpClientInstrumented: boolean = false,
49+
): void {
4550
if (skipNativeFetchCheck && !supportsNativeFetch()) {
4651
return;
4752
}
@@ -59,7 +64,9 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
5964
};
6065

6166
// if there is no callback, fetch is instrumented directly
62-
if (!onFetchResolved) {
67+
// if httpClientInstrumented is true, we are in the HttpClient instrumentation
68+
// and we may need to capture the stacktrace even when the fetch promise is resolved
69+
if (!onFetchResolved && !httpClientInstrumented) {
6370
triggerHandlers('fetch', {
6471
...handlerData,
6572
});
@@ -72,18 +79,21 @@ function instrumentFetch(onFetchResolved?: (response: Response) => void, skipNat
7279
// it means the error, that was caused by your fetch call did not
7380
// have a stack trace, so the SDK backfilled the stack trace so
7481
// you can see which fetch call failed.
75-
const virtualStackTrace = new Error().stack;
82+
const virtualError = new Error();
83+
const virtualStackTrace = virtualError.stack;
7684

7785
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
7886
return originalFetch.apply(GLOBAL_OBJ, args).then(
7987
async (response: Response) => {
8088
if (onFetchResolved) {
8189
onFetchResolved(response);
8290
} else {
91+
// Adding the stacktrace to be able to fingerprint the failed fetch event in HttpClient instrumentation
8392
triggerHandlers('fetch', {
8493
...handlerData,
8594
endTimestamp: timestampInSeconds() * 1000,
8695
response,
96+
error: httpClientInstrumented ? virtualError : undefined,
8797
});
8898
}
8999

packages/types/src/instrument.ts

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface HandlerDataXhr {
3232
xhr: SentryWrappedXMLHttpRequest;
3333
startTimestamp?: number;
3434
endTimestamp?: number;
35+
error?: unknown;
3536
}
3637

3738
interface SentryFetchData {

0 commit comments

Comments
 (0)