Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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());

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

Expand All @@ -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 },
}),
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
12 changes: 7 additions & 5 deletions packages/browser/src/integrations/fetchStreamPerformance.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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,
addFetchInstrumentationHandler,
defineIntegration,
getSanitizedUrlStringFromUrlObject,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
startInactiveSpan,
stripDataUrlContent,
Expand All @@ -21,15 +21,16 @@ 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:
*
* ```
* --------- pageload --------------------------------
* -- http.client --
* -- http.client.stream -------
* -- http.client (streaming) --
* ```
*/
export const fetchStreamPerformanceIntegration = defineIntegration(() => {
Expand Down Expand Up @@ -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',
},
});
Expand Down
Loading