diff --git a/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js index be22b53a841b..865bb18ce84c 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/sentry.client.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5/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-5/sentry.server.config.js b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js index e4cdc576c585..2b79ec0ed337 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.config.js +++ b/dev-packages/e2e-tests/test-applications/astro-5/sentry.server.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-5/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts index 2809670ff46d..7be662a30751 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/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-5', transactionEvent => { - return transactionEvent.transaction === 'GET /ssr-error'; + const spanPromise = waitForStreamedSpan('astro-5', 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: { @@ -86,41 +81,30 @@ test.describe('server-side errors', () => { const errorEventPromise = waitForError('astro-5', errorEvent => { return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error'; }); - const transactionEventApiPromise = waitForTransaction('astro-5', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error/api'; + const apiSpanPromise = waitForStreamedSpan('astro-5', span => { + return getSpanOp(span) === 'http.server' && span.name === 'GET /endpoint-error/api'; }); - const transactionEventEndpointPromise = waitForTransaction('astro-5', transactionEvent => { - return transactionEvent.transaction === 'GET /endpoint-error'; + const endpointSpanPromise = waitForStreamedSpan('astro-5', 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 expect(parentSpanId).toMatch(/[a-f0-9]{16}/); expect(parentSpanId).not.toEqual(endpointSpanId); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index b0a6a9109207..332699bfc5d9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -1,347 +1,225 @@ 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-5'; + +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-5', txnEvent => { - return txnEvent?.transaction === '/test-ssr'; - }); + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/test-ssr')); - const serverPageRequestTxnPromise = waitForTransaction('astro-5', 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', - 'url.template': '/test-ssr', - 'url.path': '/test-ssr', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-ssr$/), - }), - 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: { - app: expect.any(Object), - cloud_resource: expect.any(Object), - culture: expect.any(Object), - device: expect.any(Object), - os: 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', - 'url.full': expect.stringContaining('/test-ssr'), - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', - '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: 'node', - request: { - cookies: {}, - headers: expect.objectContaining({ - // demonstrates that request data integration can extract headers - 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.astro', - packages: expect.any(Array), - version: expect.any(String), - }, - server_name: 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.astro', type: 'string' }, + 'url.full': { value: expect.stringContaining('/test-ssr'), 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: 'gzip, deflate, br, zstd', 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-5', 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-5', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; - }); - - const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-5', 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-5', txnEvent => { - return txnEvent?.transaction?.startsWith('/user-page/') ?? false; - }); - - const serverPageRequestTxnPromise = waitForTransaction('astro-5', txnEvent => { - return txnEvent?.transaction?.startsWith('GET /user-page/') ?? false; - }); - - const serverHTTPServerRequestTxnPromise = waitForTransaction('astro-5', 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', - 'url.template': '/user-page/[userId]', - 'url.path': '/user-page/myUsername123', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/myUsername123$/), - }, - }, + 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', - 'url.full': expect.stringContaining('/user-page/myUsername123'), - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', - '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: 'gzip, deflate, br, zstd', 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.node_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.node_fetch', - 'url.full': expect.stringContaining('/api/user/myUsername123.json'), - 'url.path': '/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.node_fetch', type: 'string' }, + 'url.full': { value: expect.stringContaining('/api/user/myUsername123.json'), type: 'string' }, + 'url.path': { value: '/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', - 'url.full': expect.stringContaining('/api/user/myUsername123.json'), - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'gzip, deflate', - 'http.request.header.accept_language': '*', - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': expect.any(String), - }, - }, - }, - 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': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate', type: 'string' }, + 'http.request.header.accept_language': { value: '*', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); }); - test('sends parametrized pageload and server transaction names for catch-all routes', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-5', 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-5', 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', - 'url.template': '/catchAll/[...path]', - 'url.path': '/catchAll/hell0/whatever-do', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/catchAll\/hell0\/whatever-do$/), - }, - }, + 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', - 'url.full': expect.stringContaining('/catchAll/hell0/whatever-do'), - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', - '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: 'gzip, deflate, br, zstd', 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' }, }); }); }); @@ -349,73 +227,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-5', txnEvent => { - return txnEvent?.transaction?.startsWith('/user-page/') ?? false; - }); + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, isSegmentNamed('pageload', '/user-page/settings')); - const serverPageRequestTxnPromise = waitForTransaction('astro-5', 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', - 'url.template': '/user-page/settings', - 'url.path': '/user-page/settings', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user-page\/settings$/), - }, - }, - }, + 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', - 'url.full': expect.stringContaining('/user-page/settings'), - 'http.request.header.accept': expect.any(String), - 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd', - '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: 'gzip, deflate, br, zstd', 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-5', 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'); }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index ca1200df8b78..29febfaa3eab 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -1,21 +1,25 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { collectStreamedSpansUntilSegment, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +const APP_NAME = 'astro-5'; test.describe('tracing in static routes with server islands', () => { - test('only sends client pageload transaction and server island endpoint transaction', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { - return txnEvent.transaction === '/server-island'; - }); + test('only sends client pageload span and server island endpoint span', async ({ page }) => { + // The resource span for the server island request is a child of the pageload segment, so the + // whole trace is in hand once the segment has arrived. + const clientSpansPromise = collectStreamedSpansUntilSegment(APP_NAME, '/server-island'); - const serverIslandEndpointTxnPromise = waitForTransaction('astro-5', evt => { - return evt.transaction === 'GET /_server-islands/[name]'; + const serverIslandEndpointSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /_server-islands/[name]'; }); await page.goto('/server-island'); - const clientPageloadTxn = await clientPageloadTxnPromise; - const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; - const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + const clientSpans = await clientSpansPromise; + const clientPageloadSpan = clientSpans.find( + span => getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/server-island', + )!; const sentryTraceMetaTags = await page.locator('meta[name="sentry-trace"]').count(); expect(sentryTraceMetaTags).toBe(0); @@ -23,78 +27,43 @@ test.describe('tracing in static routes with server islands', () => { const baggageMetaTags = await page.locator('meta[name="baggage"]').count(); expect(baggageMetaTags).toBe(0); - expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); - expect(clientPageloadParentSpanId).toBeUndefined(); + expect(clientPageloadSpan.trace_id).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadSpan.parent_span_id).toBeUndefined(); - expect(clientPageloadTxn).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - 'url.template': '/server-island', - 'url.path': '/server-island', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/server-island$/), - }), - op: 'pageload', - origin: 'auto.pageload.astro', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: clientPageloadTraceId, - }, - }, - platform: 'javascript', - transaction: '/server-island', - transaction_info: { - source: 'route', - }, - type: 'transaction', + 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' }, }); - const pageloadSpans = clientPageloadTxn.spans; - - // pageload transaction contains a resource link span for the preloaded server island request - expect(pageloadSpans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - op: 'resource.link', - origin: 'auto.resource.browser.metrics', - description: expect.stringMatching(/\/_server-islands\/Avatar.*$/), - }), - ]), + // the pageload trace contains a resource link span for the preloaded server island request. + // With span streaming the resource span is named after the domain, so the URL lives in `url.full`. + const resourceLinkSpan = clientSpans.find( + span => + getSpanOp(span) === 'resource.link' && + /\/_server-islands\/Avatar.*$/.test(String(span.attributes['url.full']?.value)), ); + expect(resourceLinkSpan).toBeDefined(); + expect(resourceLinkSpan!.attributes['sentry.origin']?.value).toBe('auto.resource.browser.metrics'); - const serverIslandEndpointTxn = await serverIslandEndpointTxnPromise; + const serverIslandEndpointSpan = await serverIslandEndpointSpanPromise; - expect(serverIslandEndpointTxn).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - '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': 'gzip, deflate, br, zstd', - 'http.request.header.accept_language': 'en-US', - 'http.request.header.sec_fetch_mode': 'cors', - 'http.request.header.user_agent': expect.any(String), - }), - op: 'http.server', - origin: 'auto.http.astro', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - }, - transaction: 'GET /_server-islands/[name]', + expect(serverIslandEndpointSpan.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': { value: expect.any(String), type: 'string' }, + 'http.request.header.accept_encoding': { value: 'gzip, deflate, br, zstd', type: 'string' }, + 'http.request.header.accept_language': { value: 'en-US', type: 'string' }, + 'http.request.header.sec_fetch_mode': { value: 'cors', type: 'string' }, + 'http.request.header.user_agent': { value: expect.any(String), type: 'string' }, }); - const serverIslandEndpointTraceId = serverIslandEndpointTxn.contexts?.trace?.trace_id; - // unfortunately, the server island trace id is not the same as the client pageload trace id // this is because the server island endpoint request is made as a resource link request, // meaning our fetch instrumentation can't attach headers to the request :( - expect(serverIslandEndpointTraceId).not.toBe(clientPageloadTraceId); + expect(serverIslandEndpointSpan.trace_id).not.toBe(clientPageloadSpan.trace_id); - await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + await page.waitForTimeout(1000); // wait another sec to ensure no server span is sent }); }); diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index b6d3bb6e7b50..af781d3c8ed9 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -1,25 +1,24 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; + +const APP_NAME = 'astro-5'; test.describe('tracing in static/pre-rendered routes', () => { test('only sends client pageload span with traceId from pre-rendered tags', async ({ page }) => { - const clientPageloadTxnPromise = waitForTransaction('astro-5', txnEvent => { - return txnEvent?.transaction === '/test-static'; + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; }); - waitForTransaction('astro-5', evt => { - if (evt.platform !== 'javascript') { - throw new Error('Server transaction should not be sent'); - } - return false; + const clientPageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/test-static'; }); await page.goto('/test-static'); - const clientPageloadTxn = await clientPageloadTxnPromise; - - const clientPageloadTraceId = clientPageloadTxn.contexts?.trace?.trace_id; - const clientPageloadParentSpanId = clientPageloadTxn.contexts?.trace?.parent_span_id; + const clientPageloadSpan = await clientPageloadSpanPromise; const sentryTraceMetaTags = await page.locator('meta[name="sentry-trace"]').count(); expect(sentryTraceMetaTags).toBe(0); @@ -27,34 +26,24 @@ test.describe('tracing in static/pre-rendered routes', () => { const baggageMetaTags = await page.locator('meta[name="baggage"]').count(); expect(baggageMetaTags).toBe(0); - expect(clientPageloadTraceId).toMatch(/[a-f0-9]{32}/); - expect(clientPageloadParentSpanId).toBeUndefined(); - - expect(clientPageloadTxn).toMatchObject({ - contexts: { - trace: { - data: expect.objectContaining({ - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.astro', - 'sentry.segment.name.source': 'route', - 'url.template': '/test-static', - 'url.path': '/test-static', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/test-static$/), - }), - op: 'pageload', - origin: 'auto.pageload.astro', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - }, - platform: 'javascript', - transaction: '/test-static', - transaction_info: { - source: 'route', - }, - type: 'transaction', + expect(clientPageloadSpan.trace_id).toMatch(/[a-f0-9]{32}/); + expect(clientPageloadSpan.parent_span_id).toBeUndefined(); + + 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' }, }); - await page.waitForTimeout(1000); // wait another sec to ensure no server transaction is sent + await page.waitForTimeout(1000); // wait another sec to ensure no server span is sent + + // The route is pre-rendered, so the request never reaches the SSR middleware and no server span + // exists for it. + expect( + streamedSpans.filter( + span => + getSpanOp(span) === 'http.server' && String(span.attributes['url.path']?.value).startsWith('/test-static'), + ), + ).toEqual([]); }); });