diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts index c8261692dbfa..4310c71ccde2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts @@ -3,10 +3,11 @@ import type { AddressInfo } from 'net'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import type { SerializedStreamedSpan } from '@sentry/core'; import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'creates an http.client.stream sibling span when fetchStreamPerformanceIntegration is used', + 'creates a streaming http.client sibling span when fetchStreamPerformanceIntegration is used', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); @@ -36,9 +37,18 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - // Wait for each span type separately since they may arrive in different envelopes - const httpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); - const streamSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client.stream'); + // Wait for each span type separately since they may arrive in different envelopes. + // Both spans share the `http.client` op, so the streaming attribute is what tells them apart. + const isHttpStreamSpan = (span: SerializedStreamedSpan): boolean => + span.attributes['http.response.body.streaming']?.value === true; + const httpSpanPromise = waitForStreamedSpan( + page, + span => getSpanOp(span) === 'http.client' && !isHttpStreamSpan(span), + ); + const streamSpanPromise = waitForStreamedSpan( + page, + span => getSpanOp(span) === 'http.client' && isHttpStreamSpan(span), + ); await page.goto(url); @@ -55,6 +65,7 @@ sentryTest( 'http.method': { type: 'string', value: 'GET' }, 'url.full': { type: 'string', value: 'http://sentry-test-site.example/delayed' }, type: { type: 'string', value: 'fetch' }, + 'http.response.body.streaming': { type: 'boolean', value: true }, }), }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts index a73642b20b6f..648c9b869fd4 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts @@ -13,8 +13,10 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { const rootSpan = await transactionPromise; const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!; - const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!; - const httpStream = rootSpan.spans?.filter(span => span.op === 'http.client.stream')[0]!; + const httpGet = rootSpan.spans?.filter( + span => span.description === 'GET http://localhost:8080/sse' && !span.data?.['http.response.body.streaming'], + )[0]!; + const httpStream = rootSpan.spans?.filter(span => span.data?.['http.response.body.streaming'])[0]!; expect(sseFetchCall).toBeDefined(); expect(httpGet).toBeDefined(); @@ -26,7 +28,7 @@ test('Waits for sse streaming when creating spans', async ({ page }) => { // http.client span ends at header arrival (~0s) const httpGetDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp); - // body streaming duration is captured in the sibling http.client.stream span (~2s) + // body streaming duration is captured in the sibling streaming http.client span (~2s) const streamDuration = Math.round((httpStream.timestamp as number) - httpStream.start_timestamp); expect(resolveDuration).toBe(0); @@ -46,7 +48,9 @@ test('Waits for sse streaming when sse has been explicitly aborted', async ({ pa const rootSpan = await transactionPromise; const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!; - const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!; + const httpGet = rootSpan.spans?.filter( + span => span.description === 'GET http://localhost:8080/sse' && !span.data?.['http.response.body.streaming'], + )[0]!; expect(sseFetchCall).toBeDefined(); expect(httpGet).toBeDefined(); @@ -86,7 +90,10 @@ test('Aborts when stream takes longer than 5s, by not updating the span duration const rootSpan = await transactionPromise; const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!; - const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse-timeout')[0]!; + const httpGet = rootSpan.spans?.filter( + span => + span.description === 'GET http://localhost:8080/sse-timeout' && !span.data?.['http.response.body.streaming'], + )[0]!; expect(sseFetchCall).toBeDefined(); expect(httpGet).toBeDefined(); diff --git a/packages/browser/src/integrations/fetchStreamPerformance.ts b/packages/browser/src/integrations/fetchStreamPerformance.ts index dd3342a31cc7..69d8b853826c 100644 --- a/packages/browser/src/integrations/fetchStreamPerformance.ts +++ b/packages/browser/src/integrations/fetchStreamPerformance.ts @@ -1,4 +1,5 @@ -import { URL_FULL } from '@sentry/conventions/attributes'; +import { SENTRY_OP, URL_FULL } from '@sentry/conventions/attributes'; +import { WEB_SERVER_HTTP_CLIENT_SPAN_OP } from '@sentry/conventions/op'; import type { IntegrationFn, Span } from '@sentry/core'; import { addFetchEndInstrumentationHandler, @@ -6,7 +7,6 @@ import { defineIntegration, getSanitizedUrlStringFromUrlObject, parseStringToURLObject, - SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan, stripDataUrlContent, @@ -21,7 +21,8 @@ const STREAM_RESOLVE_FALLBACK_MS = 90_000; const STREAMING_CONTENT_TYPES = ['text/event-stream', 'application/x-ndjson', 'application/stream+json']; /** - * Tracks streamed fetch response bodies by creating an `http.client.stream` sibling span. + * Tracks streamed fetch response bodies by creating a sibling `http.client` span, + * marked with `http.response.body.streaming`. * * The regular `http.client` span ends when response headers arrive. This integration adds * a span that starts at header arrival and ends when the body fully resolves: @@ -29,7 +30,7 @@ const STREAMING_CONTENT_TYPES = ['text/event-stream', 'application/x-ndjson', 'a * ``` * --------- pageload -------------------------------- * -- http.client -- - * -- http.client.stream ------- + * -- http.client (streaming) -- * ``` */ export const fetchStreamPerformanceIntegration = defineIntegration(() => { @@ -84,7 +85,8 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => { [URL_FULL]: stripDataUrlContent(url), 'http.method': method, type: 'fetch', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client.stream', + [SENTRY_OP]: WEB_SERVER_HTTP_CLIENT_SPAN_OP, + 'http.response.body.streaming': true, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.stream', }, });