diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx index fe7c390d792a..6dd70edf6622 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/src/index.tsx @@ -18,7 +18,6 @@ import Index from './pages/Index'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/spans.test.ts new file mode 100644 index 000000000000..139ee1bb2c08 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/spans.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto(`/projects/123/views/234/567`); + + const span = await spanPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(span.name).toBe('/projects/:projectId/views/:viewId/:detailId'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/views/234/567', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), + type: 'string', + }, + }); +}); + +test('sends a pageload span with a parameterized URL - alternative route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto(`/projects/234/old-views/234/567`); + + const span = await spanPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(span.name).toBe('/projects/:projectId/old-views/:viewId/:detailId'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/old-views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/234/old-views/234/567', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), + type: 'string', + }, + }); +}); + +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + const navigationSpanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto(`/`); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); + + const linkElement = page.locator('id=navigation'); + + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationSpan.name).toBe('/projects/:projectId/views/:viewId/:detailId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/views/456/789', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), + type: 'string', + }, + }); +}); + +test('sends a navigation span with a parameterized URL - alternative route', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + const navigationSpanPromise = waitForStreamedSpan('react-router-7-cross-usage', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto(`/`); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); + + const linkElement = page.locator('id=old-navigation'); + + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationSpan.name).toBe('/projects/:projectId/old-views/:viewId/:detailId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/old-views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/old-views/345/654', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), + type: 'string', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts deleted file mode 100644 index b9365417ce69..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-cross-usage/tests/transactions.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/projects/123/views/234/567`); - - const rootSpan = await transactionPromise; - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/views/:viewId/:detailId', - 'url.path': '/projects/123/views/234/567', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), - }, - }, - }, - transaction: '/projects/:projectId/views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/projects/234/old-views/234/567`); - - const rootSpan = await transactionPromise; - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', - 'url.path': '/projects/234/old-views/234/567', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), - }, - }, - }, - transaction: '/projects/:projectId/old-views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - const navigationTxnPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; - }); - - await page.goto(`/`); - const pageloadTxn = await pageloadTxnPromise; - - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, - }); - - const linkElement = page.locator('id=navigation'); - - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/views/:viewId/:detailId', - 'url.path': '/projects/123/views/456/789', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), - }, - }, - }, - transaction: '/projects/:projectId/views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - const navigationTxnPromise = waitForTransaction('react-router-7-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; - }); - - await page.goto(`/`); - const pageloadTxn = await pageloadTxnPromise; - - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, - }); - - const linkElement = page.locator('id=old-navigation'); - - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', - 'url.path': '/projects/123/old-views/345/654', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), - }, - }, - }, - transaction: '/projects/:projectId/old-views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx index eb0b44122b62..87131036ce76 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/src/main.tsx @@ -18,7 +18,6 @@ import User from './pages/User'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/errors.test.ts index e31d3c4066d4..7736abdbad64 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ page, baseURL }) => { const errorEventPromise = waitForError('react-router-7-spa', event => { @@ -30,8 +30,8 @@ test('Sends correct error event', async ({ page, baseURL }) => { }); test('Sets correct transactionName', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); const errorEventPromise = waitForError('react-router-7-spa', event => { @@ -39,9 +39,9 @@ test('Sets correct transactionName', async ({ page }) => { }); await page.goto('/'); - const transactionEvent = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - // Only capture error once transaction was sent + // Only capture error once the pageload span was sent const exceptionButton = page.locator('id=exception-button'); await exceptionButton.click(); @@ -53,7 +53,7 @@ test('Sets correct transactionName', async ({ page }) => { expect(errorEvent.transaction).toEqual('/'); expect(errorEvent.contexts?.trace).toEqual({ - trace_id: transactionEvent.contexts?.trace?.trace_id, - span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''), + trace_id: pageloadSpan.trace_id, + span_id: expect.not.stringContaining(pageloadSpan.span_id), }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/navigation-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/navigation-trace-propagation.test.ts index e9867237903a..da621a5729e9 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/navigation-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/navigation-trace-propagation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('propagates the navigation trace (not the stale pageload trace) for a fetch in a route mount effect', async ({ page, @@ -15,22 +15,22 @@ test('propagates the navigation trace (not the stale pageload trace) for a fetch }); }); - const pageloadTxnPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/products'; + const navigationSpanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/products'; }); await page.goto('/'); - const pageloadTxn = await pageloadTxnPromise; + const pageloadSpan = await pageloadSpanPromise; await page.locator('id=navigation-products').click(); - const navigationTxn = await navigationTxnPromise; + const navigationSpan = await navigationSpanPromise; - const pageloadTraceId = pageloadTxn.contexts?.trace?.trace_id; - const navigationTraceId = navigationTxn.contexts?.trace?.trace_id; + const pageloadTraceId = pageloadSpan.trace_id; + const navigationTraceId = navigationSpan.trace_id; const propagatedTraceId = productsRequestSentryTrace?.split('-')[0]; expect(pageloadTraceId).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/spans.test.ts similarity index 57% rename from dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/spans.test.ts index 81dc4d25d274..ed4a0832515e 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-spa/tests/spans.test.ts @@ -1,69 +1,51 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/`); - const rootSpan = await transactionPromise; - - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, + const span = await spanPromise; + + expect(span.name).toBe('/'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { +test('sends a navigation span with a parameterized URL', async ({ page }) => { page.on('console', msg => console.log(msg.text())); - const pageloadTxnPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('react-router-7-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('react-router-7-spa', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; const linkElement = page.locator('id=navigation'); - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/user/5', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), - }, - }, - }, - transaction: '/user/:id', - transaction_info: { - source: 'route', - }, + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect(navigationSpan.name).toBe('/user/:id'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/user/5', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' }, }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx index 8fc9f5510160..0d928d68760a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/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 dsn: 'https://username@domain/123', tunnel: `http://localhost:3031/`, // proxy server diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts index 345ae42d0629..ebef990b9bfc 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts @@ -1,43 +1,50 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +// Under span streaming the mysql span name is the query summary, so both queries here are named +// `SELECT`. `db.query.text` is what tells them apart. test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { - const transactionPromise = waitForTransaction('react-router-8-cloudflare', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - (transactionEvent.spans?.some(span => span.op === 'db') ?? false) - ); - }); + const spansPromise = collectStreamedSpans( + 'react-router-8-cloudflare', + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'db'), + ); const res = await request.get('/performance/db-mysql'); expect(res.status()).toBe(200); - const transactionEvent = await transactionPromise; - const dbSpans = transactionEvent.spans!.filter(span => span.op === 'db'); + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); expect(firstQuery).toBeDefined(); - expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); - expect(firstQuery!.data?.['db.system.name']).toBe('mysql'); - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); - expect(firstQuery!.data?.['server.address']).toBe('127.0.0.1'); - expect(firstQuery!.data?.['server.port']).toBe(3306); - expect(firstQuery!.data?.['db.user']).toBe('root'); + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); }); -test('a nested query lands on the same transaction (async context restored)', async ({ request }) => { - const transactionPromise = waitForTransaction('react-router-8-cloudflare', transactionEvent => { - return ( - transactionEvent.contexts?.trace?.op === 'http.server' && - (transactionEvent.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 - ); - }); +test('a nested query lands on the same segment (async context restored)', async ({ request }) => { + const spansPromise = collectStreamedSpans( + 'react-router-8-cloudflare', + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, + ); const res = await request.get('/performance/db-mysql'); expect(res.status()).toBe(200); - const transactionEvent = await transactionPromise; - const descriptions = transactionEvent.spans!.filter(span => span.op === 'db').map(span => span.description); - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); - expect(descriptions).toContain('SELECT NOW()'); + const spans = await spansPromise; + + // These are scoped to the http.server segment's trace, so both queries landing here is what proves + // the nested one kept the async context. + const queryTexts = spans + .filter(span => getSpanOp(span) === 'db') + .map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts index d80e3741947c..d9a6c5451d72 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts @@ -11,7 +11,6 @@ interface Env { // `diagnostics_channel` publishers would fire with nobody subscribed. export default Sentry.withSentry( (env: Env) => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, tunnel: 'http://localhost:3031/', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx index dd76f1229e98..8bc0a6023c55 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/src/index.tsx @@ -18,7 +18,6 @@ import Index from './pages/Index'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/spans.test.ts new file mode 100644 index 000000000000..04a164b1736f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/spans.test.ts @@ -0,0 +1,132 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto(`/projects/123/views/234/567`); + + const span = await spanPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(span.name).toBe('/projects/:projectId/views/:viewId/:detailId'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/views/234/567', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), + type: 'string', + }, + }); +}); + +test('sends a pageload span with a parameterized URL - alternative route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto(`/projects/234/old-views/234/567`); + + const span = await spanPromise; + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(span.name).toBe('/projects/:projectId/old-views/:viewId/:detailId'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/old-views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/234/old-views/234/567', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), + type: 'string', + }, + }); +}); + +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + const navigationSpanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto(`/`); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); + + const linkElement = page.locator('id=navigation'); + + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationSpan.name).toBe('/projects/:projectId/views/:viewId/:detailId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/views/456/789', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), + type: 'string', + }, + }); +}); + +test('sends a navigation span with a parameterized URL - alternative route', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + const navigationSpanPromise = waitForStreamedSpan('react-router-8-cross-usage', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto(`/`); + const pageloadSpan = await pageloadSpanPromise; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); + + const linkElement = page.locator('id=old-navigation'); + + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect((await page.innerHTML('#root')).includes('Details')).toBe(true); + expect(navigationSpan.name).toBe('/projects/:projectId/old-views/:viewId/:detailId'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/projects/:projectId/old-views/:viewId/:detailId', type: 'string' }, + 'url.path': { value: '/projects/123/old-views/345/654', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), + type: 'string', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts deleted file mode 100644 index 1865b8ca16c5..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cross-usage/tests/transactions.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/projects/123/views/234/567`); - - const rootSpan = await transactionPromise; - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/views/:viewId/:detailId', - 'url.path': '/projects/123/views/234/567', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/234\/567$/), - }, - }, - }, - transaction: '/projects/:projectId/views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a pageload transaction with a parameterized URL - alternative route', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - await page.goto(`/projects/234/old-views/234/567`); - - const rootSpan = await transactionPromise; - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', - 'url.path': '/projects/234/old-views/234/567', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/234\/old-views\/234\/567$/), - }, - }, - }, - transaction: '/projects/:projectId/old-views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - const navigationTxnPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; - }); - - await page.goto(`/`); - const pageloadTxn = await pageloadTxnPromise; - - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, - }); - - const linkElement = page.locator('id=navigation'); - - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/views/:viewId/:detailId', - 'url.path': '/projects/123/views/456/789', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/views\/456\/789$/), - }, - }, - }, - transaction: '/projects/:projectId/views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); - -test('sends a navigation transaction with a parameterized URL - alternative route', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; - }); - - const navigationTxnPromise = waitForTransaction('react-router-8-cross-usage', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; - }); - - await page.goto(`/`); - const pageloadTxn = await pageloadTxnPromise; - - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, - }); - - const linkElement = page.locator('id=old-navigation'); - - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect((await page.innerHTML('#root')).includes('Details')).toBe(true); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/projects/:projectId/old-views/:viewId/:detailId', - 'url.path': '/projects/123/old-views/345/654', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/projects\/123\/old-views\/345\/654$/), - }, - }, - }, - transaction: '/projects/:projectId/old-views/:viewId/:detailId', - transaction_info: { - source: 'route', - }, - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx index eb0b44122b62..87131036ce76 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/src/main.tsx @@ -18,7 +18,6 @@ import User from './pages/User'; const replay = Sentry.replayIntegration(); Sentry.init({ - traceLifecycle: 'static', environment: 'qa', // dynamic sampling bias to keep transactions dsn: import.meta.env.PUBLIC_E2E_TEST_DSN, integrations: [ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/errors.test.ts index 97a147f065e0..32b1f1e0a5fb 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/errors.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends correct error event', async ({ page, baseURL }) => { const errorEventPromise = waitForError('react-router-8-spa', event => { @@ -30,8 +30,8 @@ test('Sends correct error event', async ({ page, baseURL }) => { }); test('Sets correct transactionName', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); const errorEventPromise = waitForError('react-router-8-spa', event => { @@ -39,9 +39,9 @@ test('Sets correct transactionName', async ({ page }) => { }); await page.goto('/'); - const transactionEvent = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - // Only capture error once transaction was sent + // Only capture error once the pageload span was sent const exceptionButton = page.locator('id=exception-button'); await exceptionButton.click(); @@ -53,7 +53,7 @@ test('Sets correct transactionName', async ({ page }) => { expect(errorEvent.transaction).toEqual('/'); expect(errorEvent.contexts?.trace).toEqual({ - trace_id: transactionEvent.contexts?.trace?.trace_id, - span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''), + trace_id: pageloadSpan.trace_id, + span_id: expect.not.stringContaining(pageloadSpan.span_id), }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/navigation-trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/navigation-trace-propagation.test.ts index 921c20dee117..a20c67db4438 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/navigation-trace-propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/navigation-trace-propagation.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('propagates the navigation trace (not the stale pageload trace) for a fetch in a route mount effect', async ({ page, @@ -15,22 +15,22 @@ test('propagates the navigation trace (not the stale pageload trace) for a fetch }); }); - const pageloadTxnPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'navigation' && transactionEvent.transaction === '/products'; + const navigationSpanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'navigation' && span.is_segment && span.name === '/products'; }); await page.goto('/'); - const pageloadTxn = await pageloadTxnPromise; + const pageloadSpan = await pageloadSpanPromise; await page.locator('id=navigation-products').click(); - const navigationTxn = await navigationTxnPromise; + const navigationSpan = await navigationSpanPromise; - const pageloadTraceId = pageloadTxn.contexts?.trace?.trace_id; - const navigationTraceId = navigationTxn.contexts?.trace?.trace_id; + const pageloadTraceId = pageloadSpan.trace_id; + const navigationTraceId = navigationSpan.trace_id; const propagatedTraceId = productsRequestSentryTrace?.split('-')[0]; expect(pageloadTraceId).toBeDefined(); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/spans.test.ts similarity index 57% rename from dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts rename to dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/spans.test.ts index d4f229597f6e..bf401e9f9ac2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-spa/tests/spans.test.ts @@ -1,69 +1,51 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); await page.goto(`/`); - const rootSpan = await transactionPromise; - - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: '/', - transaction_info: { - source: 'route', - }, + const span = await spanPromise; + + expect(span.name).toBe('/'); + expect(span.attributes).toMatchObject({ + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { +test('sends a navigation span with a parameterized URL', async ({ page }) => { page.on('console', msg => console.log(msg.text())); - const pageloadTxnPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; }); - const navigationTxnPromise = waitForTransaction('react-router-8-spa', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('react-router-8-spa', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; const linkElement = page.locator('id=navigation'); - const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]); - - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.react.reactrouter', - data: { - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/user/5', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), - }, - }, - }, - transaction: '/user/:id', - transaction_info: { - source: 'route', - }, + const [_, navigationSpan] = await Promise.all([linkElement.click(), navigationSpanPromise]); + + expect(navigationSpan.name).toBe('/user/:id'); + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter', type: 'string' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/user/5', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), type: 'string' }, }); });