diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.client.tsx index 0008ebe5c7c8..6b9287e6f709 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/entry.client.tsx @@ -5,7 +5,6 @@ import { useEffect } from 'react'; import { hydrateRoot } from 'react-dom/client'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions // Could not find a working way to set the DSN in the browser side from the environment variables dsn: 'https://public@dsn.ingest.sentry.io/1337', diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/functions/_middleware.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/functions/_middleware.ts index 1b096dfe162b..da0554ba8e56 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/functions/_middleware.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/app/functions/_middleware.ts @@ -9,7 +9,6 @@ import * as build from '../build/server'; export const onRequest = [ (context: EventPluginContext) => sentryPagesPlugin({ - traceLifecycle: 'static', dsn: context.env.E2E_TEST_DSN, tracesSampleRate: 1.0, })(context), diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/server.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/server.ts index 0423e657d2bb..372e584e0a9e 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/server.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/server.ts @@ -36,7 +36,6 @@ export default { return wrapRequestHandler( { options: { - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-errors.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-errors.test.ts index 531bfd82f3f0..927583a4d6a8 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-errors.test.ts @@ -1,12 +1,12 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends a client-side exception to Sentry', async ({ page }) => { - // The pageload transaction only completes once the client SDK and Remix have hydrated. + // The pageload span only completes once the client SDK and Remix have hydrated. // Awaiting it before clicking guarantees the button's onClick handler is attached — a click // that lands before hydration would do nothing, and the exception would never be captured. - const pageloadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; + const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); const errorPromise = waitForError('remix-hydrogen', errorEvent => { @@ -15,7 +15,7 @@ test('Sends a client-side exception to Sentry', async ({ page }) => { await page.goto('/'); - await pageloadTransactionPromise; + await pageloadSpanPromise; const exceptionButton = page.locator('id=exception-button'); await exceptionButton.click(); diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts index 776abb4fa806..a2fec8ea9d6a 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/client-transactions.test.ts @@ -1,67 +1,50 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('Sends a pageload transaction to Sentry', async ({ page }) => { - const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; +test('Sends a pageload span to Sentry', async ({ page }) => { + const spanPromise = waitForStreamedSpan('remix-hydrogen', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); await page.goto('/'); - const transactionEvent = await transactionPromise; - - expect(transactionEvent).toBeDefined(); - expect(transactionEvent).toMatchObject({ - transaction: '/', - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - 'url.path': '/', - 'url.template': '/', - }, - }, - }, + const span = await spanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, }); }); -test('Sends a navigation transaction to Sentry', async ({ page }) => { - // Wait for the initial pageload transaction first. This ensures the client SDK and - // Remix router are fully hydrated before we click the link. Clicking before hydration - // completes makes the `` behave like a plain anchor, triggering a full page - // navigation (a `pageload` transaction) instead of a client-side `navigation` one, - // which makes this test flaky. - const pageloadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.transaction === '/'; +test('Sends a navigation span to Sentry', async ({ page }) => { + // Wait for the initial pageload span first. This ensures the client SDK and Remix router are + // fully hydrated before we click the link. Clicking before hydration completes makes the `` + // behave like a plain anchor, triggering a full page navigation (a `pageload` span) instead of a + // client-side `navigation` one, which makes this test flaky. + const pageloadSpanPromise = waitForStreamedSpan('remix-hydrogen', span => { + return getSpanOp(span) === 'pageload' && span.is_segment && span.name === '/'; }); - const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/user/:id'; + const spanPromise = waitForStreamedSpan('remix-hydrogen', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/user/:id'; }); await page.goto('/'); - await pageloadTransactionPromise; + await pageloadSpanPromise; const linkElement = page.locator('id=navigation'); await linkElement.click(); - const transactionEvent = await transactionPromise; - - expect(transactionEvent).toBeDefined(); - expect(transactionEvent).toMatchObject({ - transaction: '/user/:id', - contexts: { - trace: { - data: { - 'sentry.segment.name.source': 'route', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), - 'url.path': '/user/5', - 'url.template': '/user/:id', - }, - }, - }, + const span = await spanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' }, + 'url.path': { value: '/user/5', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, }); }); diff --git a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts index b6f17bc18c37..4d66b5463e3c 100644 --- a/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/remix-hydrogen/tests/server-transactions.test.ts @@ -1,53 +1,48 @@ 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 = 'remix-hydrogen'; test.describe.configure({ mode: 'serial' }); -test('Sends parameterized transaction name to Sentry', async ({ page }) => { - const transactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server'; +test('Sends a parameterized span name to Sentry', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + // The span name is parameterized (route pattern, not the actual URL). + return getSpanOp(span) === 'http.server' && span.is_segment && span.name === 'GET /user/:id'; }); await page.goto('/user/123'); - const transaction = await transactionPromise; + const span = await spanPromise; - expect(transaction).toBeDefined(); - expect(transaction.transaction).toBe('GET /user/:id'); + expect(span.attributes['sentry.segment.name.source']?.value).toBe('route'); }); -test('Sends two linked transactions (server & client) to Sentry', async ({ page }) => { - // We use this to identify the transactions - const testTag = crypto.randomUUID(); - - const httpServerTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.tags?.['sentry_test'] === testTag; - }); - - const pageLoadTransactionPromise = waitForTransaction('remix-hydrogen', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'pageload' && transactionEvent.tags?.['sentry_test'] === testTag; +test('Sends two linked spans (server & client) to Sentry', async ({ page }) => { + // Streamed spans are buffered before they flush, so spans from an earlier page load can still be + // arriving here. The document advertises its own trace in the `sentry-trace` meta tag, so that is + // what tells this page load's spans apart rather than the op or the URL. + const streamedSpans: SerializedStreamedSpan[] = []; + void waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); + return false; }); - page.goto(`/?tag=${testTag}`); - - const pageloadTransaction = await pageLoadTransactionPromise; - const httpServerTransaction = await httpServerTransactionPromise; - - expect(pageloadTransaction).toBeDefined(); - expect(httpServerTransaction).toBeDefined(); - - const httpServerTraceId = httpServerTransaction.contexts?.trace?.trace_id; - const httpServerSpanId = httpServerTransaction.contexts?.trace?.span_id; - - const pageLoadTraceId = pageloadTransaction.contexts?.trace?.trace_id; - const pageLoadSpanId = pageloadTransaction.contexts?.trace?.span_id; + await page.goto('/'); - expect(httpServerTransaction.transaction).toBe('GET /'); - expect(pageloadTransaction.transaction).toBe('/'); + const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content'); + const [traceId] = (sentryTrace ?? '').split('-'); + expect(traceId).toMatch(/^[a-f0-9]{32}$/); - expect(httpServerTraceId).toBeDefined(); - expect(httpServerSpanId).toBeDefined(); + const findServerSegmentSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId); + await expect.poll(findServerSegmentSpan).toBeDefined(); + expect(findServerSegmentSpan()!.name).toBe('GET /'); - expect(pageLoadTraceId).toEqual(httpServerTraceId); - expect(pageLoadSpanId).not.toEqual(httpServerSpanId); + const findPageloadSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'pageload' && span.is_segment && span.trace_id === traceId); + await expect.poll(findPageloadSpan).toBeDefined(); + expect(findPageloadSpan()!.name).toBe('/'); + expect(findPageloadSpan()!.span_id).not.toBe(findServerSegmentSpan()!.span_id); });