Skip to content

Commit 44e1068

Browse files
committed
feat(browser): Attach virtual stack traces to HttpClient events.
1 parent 0ac1e10 commit 44e1068

File tree

4 files changed

+61
-21
lines changed

4 files changed

+61
-21
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

+32-14
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

@@ -127,6 +132,7 @@ function _xhrResponseHandler(
127132
xhr: XMLHttpRequest,
128133
method: string,
129134
headers: Record<string, string>,
135+
error?: unknown,
130136
): void {
131137
if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) {
132138
let requestHeaders, responseCookies, responseHeaders;
@@ -159,6 +165,7 @@ function _xhrResponseHandler(
159165
// Can't access request cookies from XHR
160166
responseHeaders,
161167
responseCookies,
168+
stacktrace: error instanceof Error ? error.stack : undefined,
162169
});
163170

164171
captureEvent(event);
@@ -283,20 +290,24 @@ function _wrapFetch(client: Client, options: HttpClientOptions): void {
283290
return;
284291
}
285292

286-
addFetchInstrumentationHandler(handlerData => {
287-
if (getClient() !== client) {
288-
return;
289-
}
293+
addFetchInstrumentationHandler(
294+
handlerData => {
295+
if (getClient() !== client) {
296+
return;
297+
}
290298

291-
const { response, args } = handlerData;
292-
const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined];
299+
const { response, args } = handlerData;
300+
const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined];
293301

294-
if (!response) {
295-
return;
296-
}
302+
if (!response) {
303+
return;
304+
}
297305

298-
_fetchResponseHandler(options, requestInfo, response as Response, requestInit);
299-
});
306+
_fetchResponseHandler(options, requestInfo, response as Response, requestInit, handlerData.error);
307+
},
308+
false,
309+
true,
310+
);
300311
}
301312

302313
/**
@@ -323,11 +334,11 @@ function _wrapXHR(client: Client, options: HttpClientOptions): void {
323334
const { method, request_headers: headers } = sentryXhrData;
324335

325336
try {
326-
_xhrResponseHandler(options, xhr, method, headers);
337+
_xhrResponseHandler(options, xhr, method, headers, handlerData.error);
327338
} catch (e) {
328339
DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e);
329340
}
330-
});
341+
}, true);
331342
}
332343

333344
/**
@@ -358,7 +369,13 @@ function _createEvent(data: {
358369
responseCookies?: Record<string, string>;
359370
requestHeaders?: Record<string, string>;
360371
requestCookies?: Record<string, string>;
372+
stacktrace?: string;
361373
}): SentryEvent {
374+
const client = getClient();
375+
const virtualStackTrace = client && data.stacktrace ? data.stacktrace : undefined;
376+
// Remove the first frame from the stack as it's the HttpClient call
377+
const stack = virtualStackTrace && client ? client.getOptions().stackParser(virtualStackTrace, 0, 1) : undefined;
378+
362379
const message = `HTTP Client Error with status code: ${data.status}`;
363380

364381
const event: SentryEvent = {
@@ -368,6 +385,7 @@ function _createEvent(data: {
368385
{
369386
type: 'Error',
370387
value: message,
388+
stacktrace: stack ? { frames: stack } : undefined,
371389
},
372390
],
373391
},

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)