diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.client.config.js index 8081a4b09547..83573d36d0be 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.client.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.client.config.js @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/astro'; Sentry.init({ - traceLifecycle: 'static', dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.server.config.js index c45704e24a6d..8f1a839bc05f 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/sentry.server.config.js @@ -3,7 +3,6 @@ import handler from '@astrojs/cloudflare/entrypoints/server'; export default Sentry.withSentry( env => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts index 751985066afd..d4219abfe827 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/db.test.ts @@ -1,43 +1,40 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpansUntilSegment, getSpanOp } from '@sentry-internal/test-utils'; + +const APP_NAME = 'astro-6-cf-workers'; test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { - const transactionPromise = waitForTransaction('astro-6-cf-workers', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - (transactionEvent.spans?.some(span => span.op === 'db') ?? false) - ); - }); + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /db-mysql'); const res = await request.get('/db-mysql'); expect(res.status()).toBe(200); - const transactionEvent = await transactionPromise; - const dbSpans = transactionEvent.spans!.filter(span => span.op === 'db'); + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); expect(firstQuery).toBeDefined(); - expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); - expect(firstQuery!.data?.['db.system.name']).toBe('mysql'); - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); - expect(firstQuery!.data?.['server.address']).toBe('127.0.0.1'); - expect(firstQuery!.data?.['server.port']).toBe(3306); - expect(firstQuery!.data?.['db.user']).toBe('root'); + // With span streaming the span name is the low-cardinality query summary; the statement stays in + // `db.query.text`. + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['db.query.text']?.value).toBe('SELECT 1 + 1 AS solution'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); }); -test('a nested query lands on the same transaction (async context restored)', async ({ request }) => { - const transactionPromise = waitForTransaction('astro-6-cf-workers', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - (transactionEvent.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 - ); - }); +test('a nested query lands on the same trace (async context restored)', async ({ request }) => { + const spansPromise = collectStreamedSpansUntilSegment(APP_NAME, 'GET /db-mysql'); const res = await request.get('/db-mysql'); expect(res.status()).toBe(200); - const transactionEvent = await transactionPromise; - const descriptions = transactionEvent.spans!.filter(span => span.op === 'db').map(span => span.description); - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); - expect(descriptions).toContain('SELECT NOW()'); + const spans = await spansPromise; + const queryTexts = spans + .filter(span => getSpanOp(span) === 'db') + .map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/errors.server.test.ts index 07a22096b1ec..a1d0709718b8 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/errors.server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test.describe('server-side errors', () => { test('captures SSR error', async ({ page }) => { @@ -7,8 +7,8 @@ test.describe('server-side errors', () => { return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')"; }); - const transactionEventPromise = waitForTransaction('astro-6-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /ssr-error'; + const spanPromise = waitForStreamedSpan('astro-6-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /ssr-error'; }); // This page returns an error status code, so we need to catch the navigation error @@ -17,19 +17,14 @@ test.describe('server-side errors', () => { }); const errorEvent = await errorEventPromise; - const transactionEvent = await transactionEventPromise; + const span = await spanPromise; - expect(transactionEvent).toMatchObject({ - transaction: 'GET /ssr-error', - spans: [], - }); - - const traceId = transactionEvent.contexts?.trace?.trace_id; - const spanId = transactionEvent.contexts?.trace?.span_id; + const traceId = span.trace_id; + const spanId = span.span_id; expect(traceId).toMatch(/[a-f0-9]{32}/); expect(spanId).toMatch(/[a-f0-9]{16}/); - expect(transactionEvent.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(span.parent_span_id).toBeUndefined(); expect(errorEvent).toMatchObject({ contexts: { @@ -80,38 +75,28 @@ test.describe('server-side errors', () => { const errorEventPromise = waitForError('astro-6-cf-workers', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; }); - const transactionEventApiPromise = waitForTransaction('astro-6-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error/api'; + const apiSpanPromise = waitForStreamedSpan('astro-6-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.name === 'GET /endpoint-error/api'; }); - const transactionEventEndpointPromise = waitForTransaction('astro-6-cf-workers', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error'; + const endpointSpanPromise = waitForStreamedSpan('astro-6-cf-workers', span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /endpoint-error'; }); await page.goto('/endpoint-error'); await page.getByText('Get Data').click(); const errorEvent = await errorEventPromise; - const transactionEventApi = await transactionEventApiPromise; - const transactionEventEndpoint = await transactionEventEndpointPromise; - - expect(transactionEventEndpoint).toMatchObject({ - transaction: 'GET /endpoint-error', - spans: [], - }); + const apiSpan = await apiSpanPromise; + const endpointSpan = await endpointSpanPromise; - const traceId = transactionEventEndpoint.contexts?.trace?.trace_id; - const endpointSpanId = transactionEventApi.contexts?.trace?.span_id; + const traceId = endpointSpan.trace_id; + const endpointSpanId = apiSpan.span_id; expect(traceId).toMatch(/[a-f0-9]{32}/); expect(endpointSpanId).toMatch(/[a-f0-9]{16}/); - expect(transactionEventApi).toMatchObject({ - transaction: 'GET /endpoint-error/api', - spans: [], - }); - - const spanId = transactionEventApi.contexts?.trace?.span_id; - const parentSpanId = transactionEventApi.contexts?.trace?.parent_span_id; + const spanId = apiSpan.span_id; + const parentSpanId = apiSpan.parent_span_id; expect(spanId).toMatch(/[a-f0-9]{16}/); // TODO: This is incorrect, for whatever reason, it should be the endpointSpanId ideally diff --git a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts index 16269a10de0b..8cb201e2cb70 100644 --- a/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-6-cf-workers/tests/tracing.dynamic.test.ts @@ -1,323 +1,219 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +const APP_NAME = 'astro-6-cf-workers'; + +function isSegmentNamed(op: string, name: string): (span: SerializedStreamedSpan) => boolean { + return span => getSpanOp(span) === op && span.is_segment && span.name === name; +} test.describe('tracing in dynamically rendered (ssr) routes', () => { test('sends server and client pageload spans with the same trace id', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction === '/test-ssr'; - }); + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/test-ssr')); - const serverPageRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction === 'GET /test-ssr'; - }); + const serverPageRequestSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('http.server', 'GET /test-ssr')); await page.goto('/test-ssr'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const serverPageRequestTxn = await serverPageRequestTxnPromise; - - const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; - const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; - - const serverPageRequestTraceId = serverPageRequestTxn.contexts?.trace?.trace_id; - const serverPageloadSpanId = serverPageRequestTxn.contexts?.trace?.span_id; - - expect(clientPageloadTraceId).toEqual(serverPageRequestTraceId); - expect(clientPageloadParentSpanId).toEqual(serverPageloadSpanId); - - expect(clientPageloadTxn).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - }), - op: 'pageload', - origin: 'auto.pageload.astro', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - }, - environment: 'qa', - event_id: expect.stringMatching(/[a-f0-9]{32}/), - measurements: expect.any(Object), - platform: 'javascript', - request: expect.any(Object), - sdk: { - integrations: expect.any(Array), - name: 'sentry.javascript.astro', - packages: expect.any(Array), - version: expect.any(String), - }, - spans: expect.any(Array), + const clientPageloadSpan = await clientPageloadSpanPromise; + const serverPageRequestSpan = await serverPageRequestSpanPromise; + + expect(clientPageloadSpan.trace_id).toEqual(serverPageRequestSpan.trace_id); + expect(clientPageloadSpan.parent_span_id).toEqual(serverPageRequestSpan.span_id); + + expect(clientPageloadSpan).toMatchObject({ + name: '/test-ssr', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/test-ssr', - transaction_info: { - source: 'route', - }, - type: 'transaction', + end_timestamp: expect.any(Number), + is_segment: true, + }); + expect(clientPageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.astro', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'url.template': { value: '/test-ssr', type: 'string' }, + 'url.path': { value: '/test-ssr', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/test-ssr$/), type: 'string' }, }); - expect(serverPageRequestTxn).toMatchObject({ - contexts: { - cloud_resource: expect.any(Object), - culture: expect.any(Object), - runtime: expect.any(Object), - trace: { - data: { - 'http.response.status_code': 200, - method: 'GET', - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.astro', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'br, gzip', - 'http.request.header.accept_language': 'en-US', - 'http.request.header.sec_fetch_mode': 'navigate', - 'http.request.header.user_agent': expect.any(String), - }, - op: 'http.server', - origin: 'auto.http.astro', - status: 'ok', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - }, - environment: 'qa', - event_id: expect.stringMatching(/[a-f0-9]{32}/), - platform: 'javascript', - request: { - headers: expect.objectContaining({ - accept: expect.any(String), - 'accept-encoding': expect.any(String), - 'user-agent': expect.any(String), - }), - method: 'GET', - url: expect.stringContaining('/test-ssr'), - }, - sdk: { - integrations: expect.any(Array), - name: 'sentry.javascript.cloudflare', - packages: expect.any(Array), - version: expect.any(String), - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /test-ssr', - transaction_info: { - source: 'route', - }, - type: 'transaction', + expect(serverPageRequestSpan).toMatchObject({ + name: 'GET /test-ssr', + status: 'ok', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + is_segment: true, + }); + expect(serverPageRequestSpan.attributes).toMatchObject({ + 'http.response.status_code': { value: 200, type: 'integer' }, + method: { value: 'GET', type: 'string' }, + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.astro', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.cloudflare', type: 'string' }, + // demonstrates that the request data integration can extract headers + 'http.request.header.accept': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'br, gzip', type: 'string' }, + 'http.request.header.accept_language': { value: 'en-US', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'navigate', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); }); }); test.describe('nested SSR routes (client, server, server request)', () => { /** The user-page route fetches from an endpoint and creates a deeply nested span structure: - * pageload — /user-page/myUsername123 + * pageload — /user-page/[userId] * ├── browser.** — multiple browser spans * └── browser.request — /user-page/myUsername123 * └── http.server — GET /user-page/[userId] (SSR page request) - * └── http.client — GET /api/user/myUsername123.json (executing fetch call from SSR page - span) - * └── http.server — GET /api/user/myUsername123.json (server request) + * └── http.client — GET localhost (executing fetch call from SSR page - span) + * └── http.server — GET /api/user/[userId].json (server request) */ - test('sends connected server and client pageload and request spans with the same trace id', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('/user-page/') ?? false; - }); + // Every span of this page load is identifiable on its own, so each is awaited separately. That + // keeps "they share a trace" an assertion rather than the selector the spans are looked up by. + const isApiRequestSpan = (span: SerializedStreamedSpan): boolean => + getSpanOp(span) === 'http.server' && span.name === 'GET /api/user/[userId].json'; + const isApiFetchSpan = (span: SerializedStreamedSpan): boolean => + getSpanOp(span) === 'http.client' && + String(span.attributes['url.full']?.value).includes('/api/user/myUsername123.json'); + + const waitForUserPageSpans = (): Promise< + [SerializedStreamedSpan, SerializedStreamedSpan, SerializedStreamedSpan, SerializedStreamedSpan] + > => + Promise.all([ + waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/user-page/[userId]')), + waitForStreamedSpan(APP_NAME, isSegmentNamed('http.server', 'GET /user-page/[userId]')), + waitForStreamedSpan(APP_NAME, isApiRequestSpan), + waitForStreamedSpan(APP_NAME, isApiFetchSpan), + ]); - const serverPageRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; - }); - - const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; - }); + test('sends connected server and client pageload and request spans with the same trace id', async ({ page }) => { + const spansPromise = waitForUserPageSpans(); await page.goto('/user-page/myUsername123'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const serverPageRequestTxn = await serverPageRequestTxnPromise; - const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; - const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( - span => span.op === 'http.client' && span.description?.includes('/api/user/'), - ); - - const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; + const [clientPageloadSpan, serverPageRequestSpan, serverHTTPServerRequestSpan, serverRequestHTTPClientSpan] = + await spansPromise; - // Verify all spans have the same trace ID - expect(clientPageloadTraceId).toEqual(serverPageRequestTxn.contexts?.trace?.trace_id); - expect(clientPageloadTraceId).toEqual(serverHTTPServerRequestTxn.contexts?.trace?.trace_id); - expect(clientPageloadTraceId).toEqual(serverRequestHTTPClientSpan?.trace_id); + // All four spans belong to the same trace + const traceId = serverPageRequestSpan.trace_id; + expect(clientPageloadSpan.trace_id).toEqual(traceId); + expect(serverHTTPServerRequestSpan.trace_id).toEqual(traceId); + expect(serverRequestHTTPClientSpan.trace_id).toEqual(traceId); // serverPageRequest has no parent (root span) - expect(serverPageRequestTxn.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(serverPageRequestSpan.parent_span_id).toBeUndefined(); // clientPageload's parent and serverRequestHTTPClient's parent is serverPageRequest - const serverPageRequestSpanId = serverPageRequestTxn.contexts?.trace?.span_id; - expect(clientPageloadTxn.contexts?.trace?.parent_span_id).toEqual(serverPageRequestSpanId); - expect(serverRequestHTTPClientSpan?.parent_span_id).toEqual(serverPageRequestSpanId); + expect(clientPageloadSpan.parent_span_id).toEqual(serverPageRequestSpan.span_id); + expect(serverRequestHTTPClientSpan.parent_span_id).toEqual(serverPageRequestSpan.span_id); // serverHTTPServerRequest's parent is serverRequestHTTPClient - expect(serverHTTPServerRequestTxn.contexts?.trace?.parent_span_id).toEqual(serverRequestHTTPClientSpan?.span_id); + expect(serverHTTPServerRequestSpan.parent_span_id).toEqual(serverRequestHTTPClientSpan.span_id); }); - test('sends parametrized pageload, server and API request transaction names', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('/user-page/') ?? false; - }); - - const serverPageRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; - }); - - const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /api/user/') ?? false; - }); + test('sends parametrized pageload, server and API request span names', async ({ page }) => { + const spansPromise = waitForUserPageSpans(); await page.goto('/user-page/myUsername123'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const serverPageRequestTxn = await serverPageRequestTxnPromise; - const serverHTTPServerRequestTxn = await serverHTTPServerRequestTxnPromise; - - const serverRequestHTTPClientSpan = serverPageRequestTxn.spans?.find( - span => span.op === 'http.client' && span.description?.includes('/api/user/'), - ); - const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); expect(routeNameMetaContent).toBe('%2Fuser-page%2F%5BuserId%5D'); - // Client pageload transaction - actual URL with pageload operation - expect(clientPageloadTxn).toMatchObject({ - transaction: '/user-page/[userId]', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.astro', - data: { - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - }, - }, + const [clientPageloadSpan, serverPageRequestSpan, serverHTTPServerRequestSpan, serverRequestHTTPClientSpan] = + await spansPromise; + + // Client pageload span - parametrized route with pageload operation + expect(clientPageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user-page/[userId]', type: 'string' }, + 'url.path': { value: '/user-page/myUsername123', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/myUsername123$/), + type: 'string', }, }); - // Server page request transaction - parametrized transaction name with actual URL in data - expect(serverPageRequestTxn).toMatchObject({ - transaction: 'GET /user-page/[userId]', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.astro', - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.astro', - 'sentry.segment.name.source': 'route', - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'br, gzip', - 'http.request.header.accept_language': 'en-US', - 'http.request.header.sec_fetch_mode': 'navigate', - 'http.request.header.user_agent': expect.any(String), - }, - }, - }, - request: { url: expect.stringContaining('/user-page/myUsername123') }, + // Server page request span - parametrized span name with the actual URL in the attributes + expect(serverPageRequestSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.full': { value: expect.stringContaining('/user-page/myUsername123'), type: 'string' }, + 'http.request.header.accept': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'br, gzip', type: 'string' }, + 'http.request.header.accept_language': { value: 'en-US', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'navigate', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); - // HTTP client span - actual API URL with client operation - expect(serverRequestHTTPClientSpan).toMatchObject({ - op: 'http.client', - origin: 'auto.http.fetch', - description: 'GET http://localhost:3030/api/user/myUsername123.json', // http.client does not need to be parametrized - data: { - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.fetch', - 'url.full': 'http://localhost:3030/api/user/myUsername123.json', - }, + // HTTP client span - with span streaming only the domain is kept in the name, the URL lives in + // the attributes + expect(serverRequestHTTPClientSpan.name).toBe('GET localhost'); + expect(serverRequestHTTPClientSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'http.client', type: 'string' }, + 'sentry.origin': { value: 'auto.http.fetch', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/api/user/myUsername123.json', type: 'string' }, }); - // Server HTTP request transaction - expect(serverHTTPServerRequestTxn).toMatchObject({ - transaction: 'GET /api/user/[userId].json', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.astro', - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.astro', - 'sentry.segment.name.source': 'route', - 'http.request.header.accept_encoding': 'br, gzip', - }, - }, - }, - request: { url: expect.stringContaining('/api/user/myUsername123.json') }, + // Server HTTP request span + expect(serverHTTPServerRequestSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'http.request.header.accept_encoding': { value: 'br, gzip', type: 'string' }, }); }); - test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('/catchAll/') ?? false; - }); + test('sends parametrized pageload and server span names for catch-all routes', async ({ page }) => { + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/catchAll/[...path]')); - const serverPageRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /catchAll/') ?? false; - }); + const serverPageRequestSpanPromise = waitForStreamedSpan( + APP_NAME, + isSegmentNamed('http.server', 'GET /catchAll/[...path]'), + ); await page.goto('/catchAll/hell0/whatever-do'); const routeNameMetaContent = await page.locator('meta[name="sentry-route-name"]').getAttribute('content'); expect(routeNameMetaContent).toBe('%2FcatchAll%2F%5B...path%5D'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const serverPageRequestTxn = await serverPageRequestTxnPromise; - - expect(clientPageloadTxn).toMatchObject({ - transaction: '/catchAll/[...path]', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.astro', - data: { - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - }, - }, + const clientPageloadSpan = await clientPageloadSpanPromise; + const serverPageRequestSpan = await serverPageRequestSpanPromise; + + expect(clientPageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/catchAll/[...path]', type: 'string' }, + 'url.path': { value: '/catchAll/hell0/whatever-do', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/catchAll\/hell0\/whatever-do$/), + type: 'string', }, }); - expect(serverPageRequestTxn).toMatchObject({ - transaction: 'GET /catchAll/[...path]', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.astro', - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.astro', - 'sentry.segment.name.source': 'route', - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'br, gzip', - 'http.request.header.accept_language': 'en-US', - 'http.request.header.sec_fetch_mode': 'navigate', - 'http.request.header.user_agent': expect.any(String), - }, - }, - }, - request: { url: expect.stringContaining('/catchAll/hell0/whatever-do') }, + expect(serverPageRequestSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.full': { value: expect.stringContaining('/catchAll/hell0/whatever-do'), type: 'string' }, + 'http.request.header.accept': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'br, gzip', type: 'string' }, + 'http.request.header.accept_language': { value: 'en-US', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'navigate', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); }); }); @@ -325,69 +221,47 @@ test.describe('nested SSR routes (client, server, server request)', () => { // Case for `user-page/[id]` vs. `user-page/settings` static routes test.describe('parametrized vs static paths', () => { test('should use static route name for static route in parametrized path', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('/user-page/') ?? false; - }); + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/user-page/settings')); - const serverPageRequestTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; - }); + const serverPageRequestSpanPromise = waitForStreamedSpan( + APP_NAME, + isSegmentNamed('http.server', 'GET /user-page/settings'), + ); await page.goto('/user-page/settings'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const serverPageRequestTxn = await serverPageRequestTxnPromise; - - expect(clientPageloadTxn).toMatchObject({ - transaction: '/user-page/settings', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.astro', - data: { - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - }, - }, - }, + const clientPageloadSpan = await clientPageloadSpanPromise; + const serverPageRequestSpan = await serverPageRequestSpanPromise; + + expect(clientPageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user-page/settings', type: 'string' }, + 'url.path': { value: '/user-page/settings', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/settings$/), type: 'string' }, }); - expect(serverPageRequestTxn).toMatchObject({ - transaction: 'GET /user-page/settings', - transaction_info: { source: 'route' }, - contexts: { - trace: { - op: 'http.server', - origin: 'auto.http.astro', - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.astro', - 'sentry.segment.name.source': 'route', - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'br, gzip', - 'http.request.header.accept_language': 'en-US', - 'http.request.header.sec_fetch_mode': 'navigate', - 'http.request.header.user_agent': expect.any(String), - }, - }, - }, - request: { url: expect.stringContaining('/user-page/settings') }, + expect(serverPageRequestSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.astro', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.full': { value: expect.stringContaining('/user-page/settings'), type: 'string' }, + 'http.request.header.accept': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'br, gzip', type: 'string' }, + 'http.request.header.accept_language': { value: 'en-US', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'navigate', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); }); test('allows for span name override via beforeStartSpan', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-6-cf-workers', txnEvent => { - return txnEvent?.transaction?.startsWith('/blog/') ?? false; - }); + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/blog/my-post')); await page.goto('/blog/my-post'); - const clientPageloadTxn = await clientPageloadTxnPromise; - expect(clientPageloadTxn).toMatchObject({ - transaction: '/blog/my-post', - transaction_info: { source: 'custom' }, - }); + const clientPageloadSpan = await clientPageloadSpanPromise; + + expect(clientPageloadSpan.attributes['sentry.segment.name.source']?.value).toBe('custom'); }); });