diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 9cbf45563f0b..84d5b5c176f9 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import type { Client, + HandlerDataFetch, HandlerDataXhr, RequestHookInfo, ResponseHookInfo, @@ -124,7 +125,7 @@ export interface RequestInstrumentationOptions { } const responseToSpanId = new WeakMap(); -const spanIdToEndTimestamp = new Map(); +const spanIdToDeferredData = new Map(); export const defaultRequestInstrumentationOptions: RequestInstrumentationOptions = { traceFetch: true, @@ -159,44 +160,46 @@ export function instrumentOutgoingRequests(client: Client, _options?: Partial { - if (event.type === 'transaction' && event.spans) { - event.spans.forEach(span => { - if (span.op === 'http.client') { - const updatedTimestamp = spanIdToEndTimestamp.get(span.span_id); - if (updatedTimestamp) { - span.timestamp = updatedTimestamp / 1000; - spanIdToEndTimestamp.delete(span.span_id); - } - } - }); - } - return event; - }); - if (trackFetchStreamPerformance) { addFetchEndInstrumentationHandler(handlerData => { if (handlerData.response) { - const span = responseToSpanId.get(handlerData.response); - if (span && handlerData.endTimestamp) { - spanIdToEndTimestamp.set(span, handlerData.endTimestamp); + const spanId = responseToSpanId.get(handlerData.response); + if (spanId) { + const deferred = spanIdToDeferredData.get(spanId); + if (deferred && handlerData.endTimestamp) { + spans[spanId] = deferred.span; + deferred.handlerData.endTimestamp = handlerData.endTimestamp; + instrumentFetchRequest(deferred.handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans, { + propagateTraceparent, + onRequestSpanEnd, + }); + spanIdToDeferredData.delete(spanId); + } } } }); } addFetchInstrumentationHandler(handlerData => { + // When tracking streaming performance, defer span end until the response body resolves. + // We intercept the end call, save the span, and let the fetchEndInstrumentationHandler + // end it with the correct timestamp. + if (trackFetchStreamPerformance && handlerData.endTimestamp && handlerData.response) { + const spanId = handlerData.fetchData?.__span; + if (spanId && spans[spanId]) { + responseToSpanId.set(handlerData.response, spanId); + spanIdToDeferredData.set(spanId, { span: spans[spanId], handlerData }); + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete spans[spanId]; + return; + } + } + const createdSpan = instrumentFetchRequest(handlerData, shouldCreateSpan, shouldAttachHeadersWithTargets, spans, { propagateTraceparent, onRequestSpanEnd, }); - if (handlerData.response && handlerData.fetchData.__span) { - responseToSpanId.set(handlerData.response, handlerData.fetchData.__span); - } - // We cannot use `window.location` in the generic fetch instrumentation, // but we need it for reliable `server.address` attribute. // so we extend this in here diff --git a/packages/browser/test/tracing/request.test.ts b/packages/browser/test/tracing/request.test.ts index 1674a96d1937..4bdd3886b275 100644 --- a/packages/browser/test/tracing/request.test.ts +++ b/packages/browser/test/tracing/request.test.ts @@ -12,11 +12,6 @@ beforeAll(() => { }); class MockClient implements Partial { - public addEventProcessor: () => void; - constructor() { - // Mock addEventProcessor function - this.addEventProcessor = vi.fn(); - } // @ts-expect-error not returning options for the test public getOptions() { return {};