diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/app/entry.client.tsx index fb6b19851227..249e18d27c08 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/app/entry.client.tsx @@ -4,7 +4,6 @@ import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions // todo: get this from env dsn: 'https://username@domain/123', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/navigation.client.test.ts index bbe7bf9a4907..91f21bc32ec7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/navigation.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/navigation.client.test.ts @@ -1,66 +1,46 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - navigation performance', () => { - test('should update navigation transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should update navigation span for dynamic routes', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation - const transaction = await txPromise; + const span = await navigationSpanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/pageload.client.test.ts index 0c37841fcada..10a270928ece 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-spa/tests/performance/pageload.client.test.ts @@ -1,116 +1,63 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - pageload performance', () => { - test('should send pageload transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + test('should send pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); await page.getByRole('heading', { name: 'Performance Page' }).waitFor(); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance', - 'url.path': '/performance', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance', type: 'string' }, + 'url.path': { value: '/performance', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance$/), type: 'string' }, }); }); - test('should update pageload transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'pageload' - ); + test('should update pageload span for dynamic routes', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/with/sentry`); await page.getByRole('heading', { name: 'Dynamic Parameter Page' }).waitFor(); - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + const span = await spanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx index d4a0e1d0900b..005268b40ad0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx @@ -4,7 +4,6 @@ import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions // todo: get this from env dsn: 'https://username@domain/123', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs index 00a6d2952286..c16240141b6d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts index 0664ac5c99e1..a1f44a541709 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,19 +1,18 @@ 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'; import { APP_NAME } from '../constants'; -test.describe('low-quality transaction filter', () => { - test('does not send a server transaction for /__manifest? requests', async ({ page }) => { - const serverTxns: Array<{ contexts?: { trace?: { data?: Record } } }> = []; +test.describe('low-quality span filter', () => { + test('does not send a server span for /__manifest? requests', async ({ page }) => { + const streamedSpans: SerializedStreamedSpan[] = []; - const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + const navigationPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); - waitForTransaction(APP_NAME, async evt => { - serverTxns.push(evt); + waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); return false; }); @@ -23,12 +22,13 @@ test.describe('low-quality transaction filter', () => { await navigationPromise; - // Force the server to flush any in-flight transactions before we assert + // Force the server to flush any in-flight spans before we assert await page.evaluate(() => fetch('/__sentry-flush')); - const targetIsManifest = (t: (typeof serverTxns)[number]) => - typeof t.contexts?.trace?.data?.['url.path'] === 'string' && - (t.contexts.trace.data['url.path'] as string).includes('/__manifest'); - expect(serverTxns.some(targetIsManifest)).toBe(false); + const targetIsManifest = (span: SerializedStreamedSpan) => { + const urlPath = span.attributes['url.path']?.value; + return typeof urlPath === 'string' && urlPath.includes('/__manifest'); + }; + expect(streamedSpans.some(targetIsManifest)).toBe(false); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts index dbce05350ad9..f3022fede097 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/middleware.server.test.ts @@ -1,38 +1,33 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - middleware', () => { - test('should send middleware transaction on pageload', async ({ page }) => { - const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/with-middleware'; - }); - - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance/with-middleware'; - }); - - const customMiddlewareTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'authMiddleware'; + test('should send middleware span on pageload', async ({ page }) => { + const serverSpansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => span.name === 'GET /performance/with-middleware' && span.is_segment) && + spansOfTrace.some(span => span.name === 'authMiddleware'), + ); + + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with-middleware' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/with-middleware`); - const serverTx = await serverTxPromise; - const pageloadTx = await pageloadTxPromise; - const customMiddlewareTx = await customMiddlewareTxPromise; + const serverSpans = await serverSpansPromise; + const pageloadSpan = await pageloadSpanPromise; - const traceIds = { - server: serverTx?.contexts?.trace?.trace_id, - pageload: pageloadTx?.contexts?.trace?.trace_id, - customMiddleware: customMiddlewareTx?.contexts?.trace?.trace_id, - }; + const serverSpan = serverSpans.find(span => span.is_segment)!; + const customMiddlewareSpan = serverSpans.find(span => span.name === 'authMiddleware')!; - expect(pageloadTx).toBeDefined(); - expect(customMiddlewareTx).toBeDefined(); + expect(pageloadSpan).toBeDefined(); + expect(customMiddlewareSpan).toBeDefined(); - // Assert that all transactions belong to the same trace - expect(traceIds.server).toBe(traceIds.pageload); - expect(traceIds.server).toBe(traceIds.customMiddleware); + // Assert that all spans belong to the same trace + expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id); + expect(serverSpan.trace_id).toBe(customMiddlewareSpan.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/navigation.client.test.ts index 59b2f8a9e038..e203c87b9664 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/navigation.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/navigation.client.test.ts @@ -1,243 +1,174 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - navigation performance', () => { - test('should create navigation transaction', async ({ page }) => { - const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should create navigation span', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation - const transaction = await navigationPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/ssr', - 'url.path': '/performance/ssr', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), + const span = await navigationSpanPromise; + + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/ssr', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/ssr'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/ssr', type: 'string' }, + 'url.path': { value: '/performance/ssr', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/), type: 'string' }, }); }); - test('should create navigation transaction when navigating with object `to` prop', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should create navigation span when navigating with object `to` prop', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'Object Navigate' }).click(); // navigation with object to - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/object-nav', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/object-nav\?foo=bar$/), - }, - }, + const span = await navigationSpanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/object-nav', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/object-nav\?foo=bar$/), + type: 'string', }, - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, }); }); - test('should create navigation transaction when navigating with search-only object `to` prop', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'navigation'; + test('should create navigation span when navigating with search-only object `to` prop', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'Search Only Navigate' }).click(); // navigation with search-only object to - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'url.template': '/performance', - // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), - }, - }, + const span = await navigationSpanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'url.template': { value: '/performance', type: 'string' }, + // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), + type: 'string', }, - transaction: '/performance', - type: 'transaction', }); }); - test('should update navigation transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should update navigation span for dynamic routes', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), + const span = await navigationSpanPromise; + + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); - test('should create navigation transaction for navigate(-1) with correct url attributes', async ({ page }) => { - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + test('should create navigation span for navigate(-1) with correct url attributes', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); - await pageloadTxPromise; + await pageloadSpanPromise; - const forwardNavPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + const forwardNavPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); await page.getByRole('link', { name: 'SSR Page' }).click(); await forwardNavPromise; - const backNavPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'navigation'; + const backNavPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'navigation' && span.is_segment; }); await page.getByRole('button', { name: 'History Back Navigate' }).click(); - const transaction = await backNavPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'sentry.segment.name.source': 'route', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react_router', - 'url.template': '/performance', - // react-router-serve 301-redirects the bare index route to a trailing slash - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), - }, - }, - }, - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, + const span = await backNavPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'url.template': { value: '/performance', type: 'string' }, + // react-router-serve 301-redirects the bare index route to a trailing slash + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts index e3bea05a36ef..15ece1778f40 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts @@ -1,148 +1,93 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - pageload performance', () => { - test('should send pageload transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + test('should send pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance', - // react-router-serve 301-redirects the bare index route to a trailing slash - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance', type: 'string' }, + // react-router-serve 301-redirects the bare index route to a trailing slash + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); }); - test('should update pageload transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'pageload' - ); + test('should update pageload span for dynamic routes', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/with/sentry`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); - test('should send pageload transaction for prerendered pages', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/static' && transactionEvent.contexts?.trace?.op === 'pageload' - ); + test('should send pageload span for prerendered pages', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/static' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/static`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - transaction: '/performance/static', - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/static', - // react-router-serve 301-redirects prerendered routes to a trailing slash - 'url.path': '/performance/static/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/performance/static', type: 'string' }, + // react-router-serve 301-redirects prerendered routes to a trailing slash + 'url.path': { value: '/performance/static/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts index bcfe15e49c34..432b7c60f5f0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts @@ -1,107 +1,65 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - performance', () => { - test('should send server transaction on pageload', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance'; + test('should send server span on pageload', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === 'GET /performance' && span.is_segment; }); await page.goto(`/performance`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.segment.name.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, + end_timestamp: expect.any(Number), + is_segment: true, + }); + + expect(getSpanOp(span)).toBe('http.server'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.full': { value: expect.stringContaining('/performance'), type: 'string' }, }); }); - test('should send server transaction on parameterized route', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/with/:param'; + test('should send server span on parameterized route', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === 'GET /performance/with/:param' && span.is_segment; }); await page.goto(`/performance/with/some-param`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.segment.name.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance/with/some-param'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, + end_timestamp: expect.any(Number), + is_segment: true, + }); + + expect(getSpanOp(span)).toBe('http.server'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.full': { value: expect.stringContaining('/performance/with/some-param'), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/redis.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/redis.server.test.ts index 1b82a709ab7b..9872dfc534c7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/redis.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/redis.server.test.ts @@ -1,32 +1,31 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - redis db spans', () => { - test('server loader emits db.query child spans on the http.server transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === 'GET /performance/redis' && - (transactionEvent.spans?.some(span => span.op === 'db.query') ?? false) - ); - }); + test('server loader emits db.query child spans on the http.server segment', async ({ page }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => span.name === 'GET /performance/redis' && span.is_segment) && + // loader runs SET then GET => at least two redis command spans + spansOfTrace.filter(span => getSpanOp(span) === 'db.query').length >= 2, + ); await page.goto('/performance/redis'); - const transaction = await txPromise; - - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; - // Collect every span id in the transaction (root + children) so we can verify nesting. - const rootSpanId = transaction.contexts?.trace?.span_id; - const spanIds = new Set([rootSpanId, ...(transaction.spans ?? []).map(span => span.span_id)]); + expect(getSpanOp(segmentSpan)).toBe('http.server'); - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + // Collect every span id in the trace (segment + children) so we can verify nesting. + const spanIds = new Set(spans.map(span => span.span_id)); + const redisSpans = spans.filter(span => getSpanOp(span) === 'db.query'); - // loader runs SET then GET => at least two redis command spans expect(redisSpans.length).toBeGreaterThanOrEqual(2); - // every redis span nests under the http.server transaction (its parent is part of the same span tree) + // every redis span nests under the http.server segment (its parent is part of the same span tree) const allNested = redisSpans.every( span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id), ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts index b62e784ff3e3..9ac6ccb807b1 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/trace-propagation.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('Trace propagation', () => { @@ -18,24 +19,40 @@ test.describe('Trace propagation', () => { }); test('should have trace connection', async ({ page }) => { - const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET *'; - }); - - const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/'; + // 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; }); await page.goto(`/`); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; - expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id); + const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content'); + const [traceId, handlerSpanId] = (sentryTrace ?? '').split('-'); + expect(traceId).toMatch(/^[a-f0-9]{32}$/); + expect(handlerSpanId).toMatch(/^[a-f0-9]{16}$/); + + // The client continues the server trace, so its pageload span hangs off the span the meta tag + // names. Selecting it that way, rather than by op, is what makes the trace assertion below mean + // something: a pageload that failed to continue the trace would have no parent at all. + const findClientSpan = () => + streamedSpans.find( + span => getSpanOp(span) === 'pageload' && span.is_segment && span.parent_span_id === handlerSpanId, + ); + await expect.poll(findClientSpan).toBeDefined(); + expect(findClientSpan()!.trace_id).toBe(traceId); - const requestHandlerSpan = serverTx.spans?.find(span => span.op === 'handler'); + const findServerSegmentSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId); + await expect.poll(findServerSegmentSpan).toBeDefined(); + const requestHandlerSpan = streamedSpans.find(span => span.span_id === handlerSpanId); expect(requestHandlerSpan).toBeDefined(); - expect(clientTx.contexts?.trace?.parent_span_id).toBe(requestHandlerSpan?.span_id); + expect(getSpanOp(requestHandlerSpan!)).toBe('handler'); + expect(requestHandlerSpan!.trace_id).toBe(traceId); }); test('should not have trace connection for prerendered pages', async ({ page }) => { diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.client.tsx index d4a0e1d0900b..005268b40ad0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/entry.client.tsx @@ -4,7 +4,6 @@ import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions // todo: get this from env dsn: 'https://username@domain/123', diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.mjs index 00a6d2952286..c16240141b6d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/instrument.mjs @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://username@domain/123', environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts index 0664ac5c99e1..a1f44a541709 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/low-quality-filter.server.test.ts @@ -1,19 +1,18 @@ 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'; import { APP_NAME } from '../constants'; -test.describe('low-quality transaction filter', () => { - test('does not send a server transaction for /__manifest? requests', async ({ page }) => { - const serverTxns: Array<{ contexts?: { trace?: { data?: Record } } }> = []; +test.describe('low-quality span filter', () => { + test('does not send a server span for /__manifest? requests', async ({ page }) => { + const streamedSpans: SerializedStreamedSpan[] = []; - const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + const navigationPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); - waitForTransaction(APP_NAME, async evt => { - serverTxns.push(evt); + waitForStreamedSpans(APP_NAME, spans => { + streamedSpans.push(...spans); return false; }); @@ -23,12 +22,13 @@ test.describe('low-quality transaction filter', () => { await navigationPromise; - // Force the server to flush any in-flight transactions before we assert + // Force the server to flush any in-flight spans before we assert await page.evaluate(() => fetch('/__sentry-flush')); - const targetIsManifest = (t: (typeof serverTxns)[number]) => - typeof t.contexts?.trace?.data?.['url.path'] === 'string' && - (t.contexts.trace.data['url.path'] as string).includes('/__manifest'); - expect(serverTxns.some(targetIsManifest)).toBe(false); + const targetIsManifest = (span: SerializedStreamedSpan) => { + const urlPath = span.attributes['url.path']?.value; + return typeof urlPath === 'string' && urlPath.includes('/__manifest'); + }; + expect(streamedSpans.some(targetIsManifest)).toBe(false); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/middleware.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/middleware.server.test.ts index dbce05350ad9..f3022fede097 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/middleware.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/middleware.server.test.ts @@ -1,38 +1,33 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - middleware', () => { - test('should send middleware transaction on pageload', async ({ page }) => { - const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/with-middleware'; - }); - - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance/with-middleware'; - }); - - const customMiddlewareTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'authMiddleware'; + test('should send middleware span on pageload', async ({ page }) => { + const serverSpansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => span.name === 'GET /performance/with-middleware' && span.is_segment) && + spansOfTrace.some(span => span.name === 'authMiddleware'), + ); + + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with-middleware' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/with-middleware`); - const serverTx = await serverTxPromise; - const pageloadTx = await pageloadTxPromise; - const customMiddlewareTx = await customMiddlewareTxPromise; + const serverSpans = await serverSpansPromise; + const pageloadSpan = await pageloadSpanPromise; - const traceIds = { - server: serverTx?.contexts?.trace?.trace_id, - pageload: pageloadTx?.contexts?.trace?.trace_id, - customMiddleware: customMiddlewareTx?.contexts?.trace?.trace_id, - }; + const serverSpan = serverSpans.find(span => span.is_segment)!; + const customMiddlewareSpan = serverSpans.find(span => span.name === 'authMiddleware')!; - expect(pageloadTx).toBeDefined(); - expect(customMiddlewareTx).toBeDefined(); + expect(pageloadSpan).toBeDefined(); + expect(customMiddlewareSpan).toBeDefined(); - // Assert that all transactions belong to the same trace - expect(traceIds.server).toBe(traceIds.pageload); - expect(traceIds.server).toBe(traceIds.customMiddleware); + // Assert that all spans belong to the same trace + expect(serverSpan.trace_id).toBe(pageloadSpan.trace_id); + expect(serverSpan.trace_id).toBe(customMiddlewareSpan.trace_id); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts index 59b2f8a9e038..e203c87b9664 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/navigation.client.test.ts @@ -1,243 +1,174 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - navigation performance', () => { - test('should create navigation transaction', async ({ page }) => { - const navigationPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should create navigation span', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'SSR Page' }).click(); // navigation - const transaction = await navigationPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/ssr', - 'url.path': '/performance/ssr', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), + const span = await navigationSpanPromise; + + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/ssr', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/ssr'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/ssr', type: 'string' }, + 'url.path': { value: '/performance/ssr', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/ssr$/), type: 'string' }, }); }); - test('should create navigation transaction when navigating with object `to` prop', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should create navigation span when navigating with object `to` prop', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'Object Navigate' }).click(); // navigation with object to - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/object-nav', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/object-nav\?foo=bar$/), - }, - }, + const span = await navigationSpanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/object-nav', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/object-nav\?foo=bar$/), + type: 'string', }, - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, }); }); - test('should create navigation transaction when navigating with search-only object `to` prop', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'navigation'; + test('should create navigation span when navigating with search-only object `to` prop', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'Search Only Navigate' }).click(); // navigation with search-only object to - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'url.template': '/performance', - // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), - }, - }, + const span = await navigationSpanPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'url.template': { value: '/performance', type: 'string' }, + // the initial pageload to `/performance` gets 301-redirected to a trailing slash by react-router-serve + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/\?query=test$/), + type: 'string', }, - transaction: '/performance', - type: 'transaction', }); }); - test('should update navigation transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'navigation' - ); + test('should update navigation span for dynamic routes', async ({ page }) => { + const navigationSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'navigation' && span.is_segment; }); - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); // pageload - await pageloadTxPromise; + await pageloadSpanPromise; await page.getByRole('link', { name: 'With Param Page' }).click(); // navigation - const transaction = await txPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.navigation.react_router', - 'sentry.op': 'navigation', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'navigation', - origin: 'auto.navigation.react_router', - }, - }, - spans: expect.any(Array), + const span = await navigationSpanPromise; + + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); - test('should create navigation transaction for navigate(-1) with correct url attributes', async ({ page }) => { - const pageloadTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + test('should create navigation span for navigate(-1) with correct url attributes', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); - await pageloadTxPromise; + await pageloadSpanPromise; - const forwardNavPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/ssr' && transactionEvent.contexts?.trace?.op === 'navigation' - ); + const forwardNavPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/ssr' && getSpanOp(span) === 'navigation' && span.is_segment; }); await page.getByRole('link', { name: 'SSR Page' }).click(); await forwardNavPromise; - const backNavPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'navigation'; + const backNavPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'navigation' && span.is_segment; }); await page.getByRole('button', { name: 'History Back Navigate' }).click(); - const transaction = await backNavPromise; - - expect(transaction).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react_router', - data: { - 'sentry.segment.name.source': 'route', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react_router', - 'url.template': '/performance', - // react-router-serve 301-redirects the bare index route to a trailing slash - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), - }, - }, - }, - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, + const span = await backNavPromise; + + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react_router', type: 'string' }, + 'url.template': { value: '/performance', type: 'string' }, + // react-router-serve 301-redirects the bare index route to a trailing slash + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts index e3bea05a36ef..15ece1778f40 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/pageload.client.test.ts @@ -1,148 +1,93 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('client - pageload performance', () => { - test('should send pageload transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/performance' && transactionEvent.contexts?.trace?.op === 'pageload'; + test('should send pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance', - // react-router-serve 301-redirects the bare index route to a trailing slash - 'url.path': '/performance/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], - }, - tags: { runtime: 'browser' }, + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance', type: 'string' }, + // react-router-serve 301-redirects the bare index route to a trailing slash + 'url.path': { value: '/performance/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/$/), type: 'string' }, }); }); - test('should update pageload transaction for dynamic routes', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/with/:param' && - transactionEvent.contexts?.trace?.op === 'pageload' - ); + test('should update pageload span for dynamic routes', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/with/:param' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/with/sentry`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/with/:param', - 'url.path': '/performance/with/sentry', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: '/performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - measurements: expect.any(Object), - platform: 'javascript', - request: { - url: expect.stringContaining('/performance/with/sentry'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/browser', version: expect.any(String) }, - ], + end_timestamp: expect.any(Number), + is_segment: true, + status: 'ok', + }); + + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.template': { value: '/performance/with/:param', type: 'string' }, + 'url.path': { value: '/performance/with/sentry', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/with\/sentry$/), + type: 'string', }, - tags: { runtime: 'browser' }, }); }); - test('should send pageload transaction for prerendered pages', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === '/performance/static' && transactionEvent.contexts?.trace?.op === 'pageload' - ); + test('should send pageload span for prerendered pages', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === '/performance/static' && getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/performance/static`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - transaction: '/performance/static', - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.origin': 'auto.pageload.react_router', - 'sentry.op': 'pageload', - 'sentry.segment.name.source': 'route', - 'url.template': '/performance/static', - // react-router-serve 301-redirects prerendered routes to a trailing slash - 'url.path': '/performance/static/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/), - }, - op: 'pageload', - origin: 'auto.pageload.react_router', - }, - }, + expect(span.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.pageload.react_router', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/performance/static', type: 'string' }, + // react-router-serve 301-redirects prerendered routes to a trailing slash + 'url.path': { value: '/performance/static/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/performance\/static\/$/), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts index bcfe15e49c34..432b7c60f5f0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/performance.server.test.ts @@ -1,107 +1,65 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - performance', () => { - test('should send server transaction on pageload', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance'; + test('should send server span on pageload', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === 'GET /performance' && span.is_segment; }); await page.goto(`/performance`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.segment.name.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, + end_timestamp: expect.any(Number), + is_segment: true, + }); + + expect(getSpanOp(span)).toBe('http.server'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.full': { value: expect.stringContaining('/performance'), type: 'string' }, }); }); - test('should send server transaction on parameterized route', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /performance/with/:param'; + test('should send server span on parameterized route', async ({ page }) => { + const spanPromise = waitForStreamedSpan(APP_NAME, span => { + return span.name === 'GET /performance/with/:param' && span.is_segment; }); await page.goto(`/performance/with/some-param`); - const transaction = await txPromise; + const span = await spanPromise; - expect(transaction).toMatchObject({ - contexts: { - trace: { - span_id: expect.any(String), - trace_id: expect.any(String), - data: { - 'sentry.op': 'http.server', - 'sentry.origin': 'auto.http.react_router.request_handler', - 'sentry.segment.name.source': 'route', - }, - op: 'http.server', - origin: 'auto.http.react_router.request_handler', - }, - }, - spans: expect.any(Array), + expect(span).toMatchObject({ + span_id: expect.any(String), + trace_id: expect.any(String), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - transaction: 'GET /performance/with/:param', - type: 'transaction', - transaction_info: { source: 'route' }, - platform: 'node', - request: { - url: expect.stringContaining('/performance/with/some-param'), - headers: expect.any(Object), - }, - event_id: expect.any(String), - environment: 'qa', - sdk: { - integrations: expect.arrayContaining([expect.any(String)]), - name: 'sentry.javascript.react-router', - version: expect.any(String), - packages: [ - { name: 'npm:@sentry/react-router', version: expect.any(String) }, - { name: 'npm:@sentry/node', version: expect.any(String) }, - ], - }, - tags: { - runtime: 'node', - }, + end_timestamp: expect.any(Number), + is_segment: true, + }); + + expect(getSpanOp(span)).toBe('http.server'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'http.server', type: 'string' }, + 'sentry.origin': { value: 'auto.http.react_router.request_handler', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'sentry.environment': { value: 'qa', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.react-router', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.sdk.integrations': { value: expect.arrayContaining([expect.any(String)]), type: 'array' }, + 'url.full': { value: expect.stringContaining('/performance/with/some-param'), type: 'string' }, }); }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/redis.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/redis.server.test.ts index 1b82a709ab7b..9872dfc534c7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/redis.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/redis.server.test.ts @@ -1,32 +1,31 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('server - redis db spans', () => { - test('server loader emits db.query child spans on the http.server transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === 'GET /performance/redis' && - (transactionEvent.spans?.some(span => span.op === 'db.query') ?? false) - ); - }); + test('server loader emits db.query child spans on the http.server segment', async ({ page }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => span.name === 'GET /performance/redis' && span.is_segment) && + // loader runs SET then GET => at least two redis command spans + spansOfTrace.filter(span => getSpanOp(span) === 'db.query').length >= 2, + ); await page.goto('/performance/redis'); - const transaction = await txPromise; - - expect(transaction.contexts?.trace?.op).toBe('http.server'); + const spans = await spansPromise; + const segmentSpan = spans.find(span => span.is_segment)!; - // Collect every span id in the transaction (root + children) so we can verify nesting. - const rootSpanId = transaction.contexts?.trace?.span_id; - const spanIds = new Set([rootSpanId, ...(transaction.spans ?? []).map(span => span.span_id)]); + expect(getSpanOp(segmentSpan)).toBe('http.server'); - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + // Collect every span id in the trace (segment + children) so we can verify nesting. + const spanIds = new Set(spans.map(span => span.span_id)); + const redisSpans = spans.filter(span => getSpanOp(span) === 'db.query'); - // loader runs SET then GET => at least two redis command spans expect(redisSpans.length).toBeGreaterThanOrEqual(2); - // every redis span nests under the http.server transaction (its parent is part of the same span tree) + // every redis span nests under the http.server segment (its parent is part of the same span tree) const allNested = redisSpans.every( span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id), ); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts index 2c888675429e..9ac6ccb807b1 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/trace-propagation.test.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpans } from '@sentry-internal/test-utils'; import { APP_NAME } from '../constants'; test.describe('Trace propagation', () => { @@ -18,24 +19,40 @@ test.describe('Trace propagation', () => { }); test('should have trace connection', async ({ page }) => { - const serverTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === 'GET /{*splat}'; - }); - - const clientTxPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return transactionEvent.transaction === '/'; + // 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; }); await page.goto(`/`); - const serverTx = await serverTxPromise; - const clientTx = await clientTxPromise; - expect(clientTx.contexts?.trace?.trace_id).toEqual(serverTx.contexts?.trace?.trace_id); + const sentryTrace = await page.getAttribute('meta[name="sentry-trace"]', 'content'); + const [traceId, handlerSpanId] = (sentryTrace ?? '').split('-'); + expect(traceId).toMatch(/^[a-f0-9]{32}$/); + expect(handlerSpanId).toMatch(/^[a-f0-9]{16}$/); + + // The client continues the server trace, so its pageload span hangs off the span the meta tag + // names. Selecting it that way, rather than by op, is what makes the trace assertion below mean + // something: a pageload that failed to continue the trace would have no parent at all. + const findClientSpan = () => + streamedSpans.find( + span => getSpanOp(span) === 'pageload' && span.is_segment && span.parent_span_id === handlerSpanId, + ); + await expect.poll(findClientSpan).toBeDefined(); + expect(findClientSpan()!.trace_id).toBe(traceId); - const requestHandlerSpan = serverTx.spans?.find(span => span.op === 'handler'); + const findServerSegmentSpan = () => + streamedSpans.find(span => getSpanOp(span) === 'http.server' && span.is_segment && span.trace_id === traceId); + await expect.poll(findServerSegmentSpan).toBeDefined(); + const requestHandlerSpan = streamedSpans.find(span => span.span_id === handlerSpanId); expect(requestHandlerSpan).toBeDefined(); - expect(clientTx.contexts?.trace?.parent_span_id).toBe(requestHandlerSpan?.span_id); + expect(getSpanOp(requestHandlerSpan!)).toBe('handler'); + expect(requestHandlerSpan!.trace_id).toBe(traceId); }); test('should not have trace connection for prerendered pages', async ({ page }) => {