Skip to content

Commit 8076339

Browse files
msonnbclaude
andcommitted
feat(browser)!: Use http.client span op for streamed fetch bodies
Streaming changes response behaviour, not the operation category. The sibling span is now marked with `http.response.body.streaming` instead of an `http.client.stream` op. Ref: JS-3105 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4a318cf commit 8076339

4 files changed

Lines changed: 34 additions & 12 deletions

File tree

MIGRATION.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ Span operations (`sentry.op`) were aligned with the values defined in `@sentry/c
270270
- Runloop spans now use `ui.task` instead of `ui.ember.runloop.<queue>`. The queue is available on the new `ember.runloop.queue` attribute.
271271
- Browser SDKs: Paint spans (`first-paint`, `first-contentful-paint`) now use the registered `browser.paint` op instead of `paint`. This does not affect Session Replay, whose performance entries keep their own `paint` type.
272272
- `@sentry/aws-serverless`: Lambda invocation spans now use the `function.aws` op instead of `function.aws.lambda`.
273+
- `@sentry/browser`: The sibling span created by `fetchStreamPerformanceIntegration` now uses `http.client` instead of `http.client.stream`. It is distinguished from the regular request span by the new `http.response.body.streaming` attribute.
273274

274275
If you reference these operations in dashboards, alerts, or `beforeSendSpan`, update them to the new values.
275276

dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import type { AddressInfo } from 'net';
33
import { expect } from '@playwright/test';
44
import { sentryTest } from '../../../../utils/fixtures';
55
import { shouldSkipTracingTest } from '../../../../utils/helpers';
6+
import type { SerializedStreamedSpan } from '@sentry/core';
67
import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils';
78

89
sentryTest(
9-
'creates an http.client.stream sibling span when fetchStreamPerformanceIntegration is used',
10+
'creates a streaming http.client sibling span when fetchStreamPerformanceIntegration is used',
1011
async ({ getLocalTestUrl, page }) => {
1112
sentryTest.skip(shouldSkipTracingTest());
1213

@@ -36,9 +37,18 @@ sentryTest(
3637

3738
const url = await getLocalTestUrl({ testDir: __dirname });
3839

39-
// Wait for each span type separately since they may arrive in different envelopes
40-
const httpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client');
41-
const streamSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client.stream');
40+
// Wait for each span type separately since they may arrive in different envelopes.
41+
// Both spans share the `http.client` op, so the streaming attribute is what tells them apart.
42+
const isStreamSpan = (span: SerializedStreamedSpan): boolean =>
43+
span.attributes['http.response.body.streaming']?.value === true;
44+
const httpSpanPromise = waitForStreamedSpan(
45+
page,
46+
span => getSpanOp(span) === 'http.client' && !isStreamSpan(span),
47+
);
48+
const streamSpanPromise = waitForStreamedSpan(
49+
page,
50+
span => getSpanOp(span) === 'http.client' && isStreamSpan(span),
51+
);
4252

4353
await page.goto(url);
4454

@@ -55,6 +65,7 @@ sentryTest(
5565
'http.method': { type: 'string', value: 'GET' },
5666
url: { type: 'string', value: 'http://sentry-test-site.example/delayed' },
5767
type: { type: 'string', value: 'fetch' },
68+
'http.response.body.streaming': { type: 'boolean', value: true },
5869
}),
5970
});
6071

dev-packages/e2e-tests/test-applications/react-router-6/tests/sse.test.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ test('Waits for sse streaming when creating spans', async ({ page }) => {
1313

1414
const rootSpan = await transactionPromise;
1515
const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
16-
const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!;
17-
const httpStream = rootSpan.spans?.filter(span => span.op === 'http.client.stream')[0]!;
16+
const httpGet = rootSpan.spans?.filter(
17+
span => span.description === 'GET http://localhost:8080/sse' && !span.data?.['http.response.body.streaming'],
18+
)[0]!;
19+
const httpStream = rootSpan.spans?.filter(span => span.data?.['http.response.body.streaming'])[0]!;
1820

1921
expect(sseFetchCall).toBeDefined();
2022
expect(httpGet).toBeDefined();
@@ -26,7 +28,7 @@ test('Waits for sse streaming when creating spans', async ({ page }) => {
2628
// http.client span ends at header arrival (~0s)
2729
const httpGetDuration = Math.round((httpGet.timestamp as number) - httpGet.start_timestamp);
2830

29-
// body streaming duration is captured in the sibling http.client.stream span (~2s)
31+
// body streaming duration is captured in the sibling streaming http.client span (~2s)
3032
const streamDuration = Math.round((httpStream.timestamp as number) - httpStream.start_timestamp);
3133

3234
expect(resolveDuration).toBe(0);
@@ -46,7 +48,9 @@ test('Waits for sse streaming when sse has been explicitly aborted', async ({ pa
4648

4749
const rootSpan = await transactionPromise;
4850
const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
49-
const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse')[0]!;
51+
const httpGet = rootSpan.spans?.filter(
52+
span => span.description === 'GET http://localhost:8080/sse' && !span.data?.['http.response.body.streaming'],
53+
)[0]!;
5054

5155
expect(sseFetchCall).toBeDefined();
5256
expect(httpGet).toBeDefined();
@@ -86,7 +90,10 @@ test('Aborts when stream takes longer than 5s, by not updating the span duration
8690

8791
const rootSpan = await transactionPromise;
8892
const sseFetchCall = rootSpan.spans?.filter(span => span.description === 'sse fetch call')[0]!;
89-
const httpGet = rootSpan.spans?.filter(span => span.description === 'GET http://localhost:8080/sse-timeout')[0]!;
93+
const httpGet = rootSpan.spans?.filter(
94+
span =>
95+
span.description === 'GET http://localhost:8080/sse-timeout' && !span.data?.['http.response.body.streaming'],
96+
)[0]!;
9097

9198
expect(sseFetchCall).toBeDefined();
9299
expect(httpGet).toBeDefined();

packages/browser/src/integrations/fetchStreamPerformance.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { WEB_SERVER_HTTP_CLIENT_SPAN_OP } from '@sentry/conventions/op';
12
import type { IntegrationFn, Span } from '@sentry/core';
23
import {
34
addFetchEndInstrumentationHandler,
@@ -20,15 +21,16 @@ const STREAM_RESOLVE_FALLBACK_MS = 90_000;
2021
const STREAMING_CONTENT_TYPES = ['text/event-stream', 'application/x-ndjson', 'application/stream+json'];
2122

2223
/**
23-
* Tracks streamed fetch response bodies by creating an `http.client.stream` sibling span.
24+
* Tracks streamed fetch response bodies by creating a sibling `http.client` span,
25+
* marked with `http.response.body.streaming`.
2426
*
2527
* The regular `http.client` span ends when response headers arrive. This integration adds
2628
* a span that starts at header arrival and ends when the body fully resolves:
2729
*
2830
* ```
2931
* --------- pageload --------------------------------
3032
* -- http.client --
31-
* -- http.client.stream -------
33+
* -- http.client (streaming) --
3234
* ```
3335
*/
3436
export const fetchStreamPerformanceIntegration = defineIntegration(() => {
@@ -83,7 +85,8 @@ export const fetchStreamPerformanceIntegration = defineIntegration(() => {
8385
url: stripDataUrlContent(url),
8486
'http.method': method,
8587
type: 'fetch',
86-
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client.stream',
88+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: WEB_SERVER_HTTP_CLIENT_SPAN_OP,
89+
'http.response.body.streaming': true,
8790
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.stream',
8891
},
8992
});

0 commit comments

Comments
 (0)