Skip to content

Commit

Permalink
feat(browser): Attach virtual stack traces to HttpClient events.
Browse files Browse the repository at this point in the history
  • Loading branch information
onurtemizkan committed Nov 27, 2024
1 parent 8f6dd04 commit 252e544
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 20 deletions.
17 changes: 14 additions & 3 deletions packages/browser-utils/src/instrument/xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -75,6 +85,7 @@ export function instrumentXHR(): void {
endTimestamp: timestampInSeconds() * 1000,
startTimestamp,
xhr: xhrOpenThisArg,
error: httpClientInstrumented ? virtualError : undefined,
};
triggerHandlers('xhr', handlerData);
}
Expand Down
48 changes: 35 additions & 13 deletions packages/browser/src/integrations/httpclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const _httpClientIntegration = ((options: Partial<HttpClientOptions> = {}) => {

return {
name: INTEGRATION_NAME,
setup(client): void {
setup(client: Client): void {
_wrapFetch(client, _options);
_wrapXHR(client, _options);
},
Expand All @@ -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);
Expand All @@ -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);
// });
}
}

Expand Down Expand Up @@ -151,6 +156,9 @@ function _xhrResponseHandler(
requestHeaders = headers;
}

const virtualError = new Error();
const virtualStacktrace = virtualError.stack;

const event = _createEvent({
url: xhr.responseURL,
method,
Expand All @@ -159,6 +167,7 @@ function _xhrResponseHandler(
// Can't access request cookies from XHR
responseHeaders,
responseCookies,
stacktrace: virtualStacktrace,
});

captureEvent(event);
Expand Down Expand Up @@ -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,
);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -358,7 +371,15 @@ function _createEvent(data: {
responseCookies?: Record<string, string>;
requestHeaders?: Record<string, string>;
requestCookies?: Record<string, string>;
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 = {
Expand All @@ -368,6 +389,7 @@ function _createEvent(data: {
{
type: 'Error',
value: message,
stacktrace: stack ? { frames: nonSentryStack } : undefined,
},
],
},
Expand Down
18 changes: 14 additions & 4 deletions packages/core/src/utils-hoist/instrument/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand All @@ -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;
}
Expand All @@ -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,
});
Expand All @@ -72,18 +79,21 @@ 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(
async (response: Response) => {
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,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface HandlerDataXhr {
xhr: SentryWrappedXMLHttpRequest;
startTimestamp?: number;
endTimestamp?: number;
error?: unknown;
}

interface SentryFetchData {
Expand Down

0 comments on commit 252e544

Please sign in to comment.