diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx index bec2f5b24481..c7ad16eebcf7 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/src/index.tsx @@ -15,7 +15,6 @@ import User from './pages/User'; 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-create-browser-router/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/spans.test.ts new file mode 100644 index 000000000000..5736ae47040f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/spans.test.ts @@ -0,0 +1,167 @@ +import { expect, test } from '@playwright/test'; +import { + collectStreamedSpans, + getSpanOp, + waitForStreamedSpan, + waitForStreamedSpans, +} from '@sentry-internal/test-utils'; + +const PREVIOUS_TRACE_LINK = [ + { + attributes: { + 'sentry.link.type': { value: 'previous_trace', type: 'string' }, + }, + sampled: true, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, +]; + +test('Captures a pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-browser-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/'); + + const span = await spanPromise; + + expect(span.name).toBe('/'); + expect(span.status).toBe('ok'); + expect(span.span_id).toMatch(/[a-f0-9]{16}/); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); + + expect(span.attributes).toMatchObject({ + 'device.memory.estimated_capacity': { value: expect.any(Number), type: expect.any(String) }, + 'device.processor_count': { value: expect.any(Number), type: 'integer' }, + 'network.connection.effective_type': { value: expect.any(String), type: 'string' }, + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + '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('Captures a navigation span', async ({ page }) => { + const spansPromise = waitForStreamedSpans('react-create-browser-router', spans => { + return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment); + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const spans = await spansPromise; + const navigationSpan = spans.find(span => span.is_segment)!; + + expect(navigationSpan.name).toBe('/user/:id'); + expect(navigationSpan.status).toBe('ok'); + expect(navigationSpan.span_id).toMatch(/[a-f0-9]{16}/); + expect(navigationSpan.trace_id).toMatch(/[a-f0-9]{32}/); + + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + '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' }, + }); + + expect(navigationSpan.links).toEqual(PREVIOUS_TRACE_LINK); + + // Filter out favicon spans which may or may not be present depending on the browser version + const childSpans = spans.filter( + span => !span.is_segment && !(span.attributes['url.full']?.value as string | undefined)?.includes('favicon'), + ); + expect(childSpans).toEqual([]); +}); + +test('Captures a lazy pageload span', async ({ page }) => { + const spansPromise = collectStreamedSpans('react-create-browser-router', spans => { + return spans.some(span => getSpanOp(span) === 'pageload' && span.is_segment); + }); + + await page.goto('/lazy-loaded-user/5/foo'); + + const spans = await spansPromise; + const pageloadSpan = spans.find(span => getSpanOp(span) === 'pageload' && span.is_segment)!; + + expect(pageloadSpan.name).toBe('/lazy-loaded-user/:id/:innerId'); + expect(pageloadSpan.status).toBe('ok'); + + expect(pageloadSpan.attributes).toMatchObject({ + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/lazy-loaded-user/:id/:innerId', type: 'string' }, + 'url.path': { value: '/lazy-loaded-user/5/foo', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), + type: 'string', + }, + }); + + expect(await page.innerText('id=content')).toContain('I am a lazy loaded user'); + + // One span for the outer lazy route, one for the inner one + const resourceSpans = spans.filter( + span => + span.trace_id === pageloadSpan.trace_id && + getSpanOp(span) === 'resource.script' && + span.attributes['sentry.origin']?.value === 'auto.resource.browser.metrics', + ); + expect(resourceSpans.length).toBeGreaterThanOrEqual(2); +}); + +test('Captures a lazy navigation span', async ({ page }) => { + const spansPromise = collectStreamedSpans('react-create-browser-router', spans => { + return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment); + }); + + await page.goto('/'); + const linkElement = page.locator('id=lazy-navigation'); + await linkElement.click(); + + const spans = await spansPromise; + const navigationSpan = spans.find(span => getSpanOp(span) === 'navigation' && span.is_segment)!; + + expect(navigationSpan.name).toBe('/lazy-loaded-user/:id/:innerId'); + expect(navigationSpan.status).toBe('ok'); + + expect(navigationSpan.attributes).toMatchObject({ + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/lazy-loaded-user/:id/:innerId', type: 'string' }, + 'url.path': { value: '/lazy-loaded-user/5/foo', type: 'string' }, + 'url.full': { + value: expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), + type: 'string', + }, + }); + + expect(navigationSpan.links).toEqual(PREVIOUS_TRACE_LINK); + + expect(await page.innerText('id=content')).toContain('I am a lazy loaded user'); + + // The pageload of `/` is its own trace, so the lazy-route chunks are scoped to the navigation + // trace. One span for the outer lazy route, one for the inner one. + const resourceSpans = spans.filter( + span => + span.trace_id === navigationSpan.trace_id && + getSpanOp(span) === 'resource.script' && + span.attributes['sentry.origin']?.value === 'auto.resource.browser.metrics', + ); + expect(resourceSpans.length).toBeGreaterThanOrEqual(2); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts deleted file mode 100644 index 79737d920682..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-create-browser-router/tests/transactions.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Captures a pageload transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(transactionEvent.contexts?.trace).toEqual( - expect.objectContaining({ - data: expect.objectContaining({ - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }), - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - status: 'ok', - }), - ); -}); - -test('Captures a navigation transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: expect.objectContaining({ - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/user/5', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/user\/5$/), - }), - links: [ - { - attributes: { - 'sentry.link.type': 'previous_trace', - }, - sampled: true, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ], - op: 'navigation', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.navigation.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/user/:id', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - // Filter out favicon spans which may or may not be present depending on the browser version - const spans = (transactionEvent.spans || []).filter(span => !span.description?.includes('favicon')); - expect(spans).toEqual([]); -}); - -test('Captures a lazy pageload transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/lazy-loaded-user/5/foo'); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: expect.objectContaining({ - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/lazy-loaded-user/:id/:innerId', - 'url.path': '/lazy-loaded-user/5/foo', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), - }), - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/lazy-loaded-user/:id/:innerId', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(await page.innerText('id=content')).toContain('I am a lazy loaded user'); - - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - // This one is the outer lazy route - expect.objectContaining({ - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - }), - // This one is the inner lazy route - expect.objectContaining({ - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - }), - ]), - ); -}); - -test('Captures a lazy navigation transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-browser-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=lazy-navigation'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: expect.objectContaining({ - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/lazy-loaded-user/:id/:innerId', - 'url.path': '/lazy-loaded-user/5/foo', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/lazy-loaded-user\/5\/foo$/), - }), - links: [ - { - attributes: { - 'sentry.link.type': 'previous_trace', - }, - sampled: true, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ], - op: 'navigation', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.navigation.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/lazy-loaded-user/:id/:innerId', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(await page.innerText('id=content')).toContain('I am a lazy loaded user'); - - expect(transactionEvent.spans).toEqual( - expect.arrayContaining([ - // This one is the outer lazy route - expect.objectContaining({ - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - }), - // This one is the inner lazy route - expect.objectContaining({ - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - }), - ]), - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx index 277600d3b7c6..86de5f20378d 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/src/index.tsx @@ -16,7 +16,6 @@ import Group from './pages/Group'; 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-create-hash-router/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts new file mode 100644 index 000000000000..d42b08d85a5a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/spans.test.ts @@ -0,0 +1,344 @@ +import { expect, test } from '@playwright/test'; +import { + collectStreamedSpans, + getSpanOp, + waitForStreamedSpan, + waitForStreamedSpans, +} from '@sentry-internal/test-utils'; + +const BROWSER_TIMING_OPS = [ + 'browser.dom_content_loaded_event', + 'browser.connect', + 'browser.request', + 'browser.response', +]; + +test('Captures a pageload span', async ({ page }) => { + const spansPromise = collectStreamedSpans('react-create-hash-router', spans => { + return spans.some(span => getSpanOp(span) === 'pageload' && span.is_segment); + }); + + await page.goto('/'); + + const spans = await spansPromise; + const pageloadSpan = spans.find(span => getSpanOp(span) === 'pageload' && span.is_segment)!; + + expect(pageloadSpan.name).toBe('/'); + expect(pageloadSpan.status).toBe('ok'); + expect(pageloadSpan.span_id).toMatch(/[a-f0-9]{16}/); + expect(pageloadSpan.trace_id).toMatch(/[a-f0-9]{32}/); + + // Matched partially: every streamed span also carries the common `sentry.sdk.*`, `sentry.release`, + // `sentry.environment` and segment attributes, and a pageload span additionally carries the web + // vitals. LCP is streamed as its own `ui.webvital.lcp` span once the page is hidden, so it is no + // longer part of the pageload span's attributes. + expect(pageloadSpan.attributes).toMatchObject({ + 'device.memory.estimated_capacity': { value: expect.any(Number), type: expect.any(String) }, + 'device.processor_count': { value: expect.any(Number), type: 'integer' }, + 'network.connection.effective_type': { value: expect.any(String), type: 'string' }, + 'browser.performance.time_origin': { value: expect.any(Number), type: expect.any(String) }, + 'browser.performance.navigation.activation_start': { value: expect.any(Number), type: expect.any(String) }, + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + }); + + for (const op of BROWSER_TIMING_OPS) { + expect(spans).toContainEqual( + expect.objectContaining({ + name: page.url(), + is_segment: false, + status: 'ok', + parent_span_id: pageloadSpan.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: pageloadSpan.trace_id, + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + attributes: expect.objectContaining({ + 'sentry.origin': { value: 'auto.ui.browser.metrics', type: 'string' }, + 'sentry.op': { value: op, type: 'string' }, + }), + }), + ); + } +}); + +test('Captures a navigation span', async ({ page }) => { + const spansPromise = waitForStreamedSpans('react-create-hash-router', spans => { + return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment); + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const spans = await spansPromise; + const navigationSpan = spans.find(span => span.is_segment)!; + + expect(navigationSpan.name).toBe('/user/:id'); + expect(navigationSpan.status).toBe('ok'); + + expect(navigationSpan.attributes).toMatchObject({ + 'device.memory.estimated_capacity': { value: expect.any(Number), type: expect.any(String) }, + 'device.processor_count': { value: expect.any(Number), type: 'integer' }, + 'network.connection.effective_type': { value: expect.any(String), type: 'string' }, + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^http:\/\/localhost:3030\/#\/user\/5$/), type: 'string' }, + }); + + expect(navigationSpan.links).toEqual([ + { + attributes: { + 'sentry.link.type': { value: 'previous_trace', type: 'string' }, + }, + sampled: true, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + ]); + + // Filter out favicon spans which may or may not be present depending on the browser version + const childSpans = spans.filter( + span => !span.is_segment && !(span.attributes['url.full']?.value as string | undefined)?.includes('favicon'), + ); + expect(childSpans).toEqual([]); +}); + +test('Captures a parameterized path pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/v2/post/1'); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1', type: 'string' }, + }); +}); + +test('Captures a parameterized path pageload span for nested route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/v2/post/1/featured'); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post/featured'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post/featured', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1/featured', type: 'string' }, + }); +}); + +test('Captures a parameterized path pageload span for deeply nested route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/v1/post/1/edit'); + + const span = await spanPromise; + + expect(span.name).toBe('/v1/post/:post/edit'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v1/post/:post/edit', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v1/post/1/edit', type: 'string' }, + }); +}); + +test('Captures a parameterized path pageload span for nested route with absolute path', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/v2/post/1/related'); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post/related'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post/related', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1/related', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-post-1'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span for nested route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-post-1-featured'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post/featured'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post/featured', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1/featured', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span for deeply nested route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-post-1-edit'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/v1/post/:post/edit'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v1/post/:post/edit', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v1/post/1/edit', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span for nested route with absolute path', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-post-1-related'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/v2/post/:post/related'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/v2/post/:post/related', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/v2/post/1/related', type: 'string' }, + }); +}); + +test('Captures a parameterized path pageload span for group route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/group/1'); + + const span = await spanPromise; + + expect(span.name).toBe('/group/:group/:user?'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/group/:group/:user?', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/group/1', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span for group route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-group-1'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/group/:group/:user?'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/group/:group/:user?', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/group/1', type: 'string' }, + }); +}); + +test('Captures a parameterized path pageload span for nested group route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/#/group/1/5'); + + const span = await spanPromise; + + expect(span.name).toBe('/group/:group/:user?'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/group/:group/:user?', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/group/1/5', type: 'string' }, + }); +}); + +test('Captures a parameterized path navigation span for nested group route', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-hash-router', span => { + return getSpanOp(span) === 'navigation' && span.is_segment; + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-group-1-user-5'); + await linkElement.click(); + + const span = await spanPromise; + + expect(span.name).toBe('/group/:group/:user?'); + expect(span.attributes).toMatchObject({ + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/group/:group/:user?', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: 'http://localhost:3030/#/group/1/5', type: 'string' }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts deleted file mode 100644 index 5479f1935c83..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts +++ /dev/null @@ -1,502 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Captures a pageload transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/'); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), - 'lcp.element': expect.any(String), - 'lcp.id': expect.any(String), - 'lcp.size': expect.any(Number), - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'performance.timeOrigin': expect.any(Number), - 'performance.activationStart': expect.any(Number), - 'lcp.renderTime': expect.any(Number), - 'lcp.loadTime': expect.any(Number), - 'url.template': '/', - 'url.full': 'http://localhost:3030/', - 'url.path': '/', - }, - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.dom_content_loaded_event', - }, - description: page.url(), - op: 'browser.dom_content_loaded_event', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - status: 'ok', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.connect', - }, - description: page.url(), - op: 'browser.connect', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - status: 'ok', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.request', - }, - description: page.url(), - op: 'browser.request', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - status: 'ok', - }); - expect(transactionEvent.spans).toContainEqual({ - data: { - 'sentry.origin': 'auto.ui.browser.metrics', - 'sentry.op': 'browser.response', - }, - description: page.url(), - op: 'browser.response', - parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.ui.browser.metrics', - status: 'ok', - }); -}); - -test('Captures a navigation transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: expect.objectContaining({ - deviceMemory: expect.any(String), - effectiveConnectionType: expect.any(String), - hardwareConcurrency: expect.any(String), - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/', - 'url.full': expect.stringMatching(/^http:\/\/localhost:3030\/#\/user\/5$/), - }), - links: [ - { - attributes: { - 'sentry.link.type': 'previous_trace', - }, - sampled: true, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ], - op: 'navigation', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.navigation.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/user/:id', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - // Filter out favicon spans which may or may not be present depending on the browser version - const spans = (transactionEvent.spans || []).filter(span => !span.description?.includes('favicon')); - expect(spans).toEqual([]); -}); - -test('Captures a parameterized path pageload transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/v2/post/1'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1', - }, - }, - }, - }); -}); - -test('Captures a parameterized path pageload transaction for nested route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/v2/post/1/featured'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post/featured', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post/featured', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1/featured', - }, - }, - }, - }); -}); - -test('Captures a parameterized path pageload transaction for deeply nested route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/v1/post/1/edit'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v1/post/:post/edit', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v1/post/:post/edit', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v1/post/1/edit', - }, - }, - }, - }); -}); - -test('Captures a parameterized path pageload transaction for nested route with absolute path', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/v2/post/1/related'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post/related', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post/related', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1/related', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-post-1'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction for nested route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-post-1-featured'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post/featured', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post/featured', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1/featured', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction for deeply nested route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-post-1-edit'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v1/post/:post/edit', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v1/post/:post/edit', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v1/post/1/edit', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction for nested route with absolute path', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-post-1-related'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/v2/post/:post/related', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/v2/post/:post/related', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/v2/post/1/related', - }, - }, - }, - }); -}); - -test('Captures a parameterized path pageload transaction for group route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/group/1'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/group/:group/:user?', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/group/1', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction for group route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-group-1'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/group/:group/:user?', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/group/1', - }, - }, - }, - }); -}); - -test('Captures a parameterized path pageload transaction for nested group route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/#/group/1/5'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/group/:group/:user?', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/group/1/5', - }, - }, - }, - }); -}); - -test('Captures a parameterized path navigation transaction for nested group route', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-hash-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-group-1-user-5'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toMatchObject({ - transaction: '/group/:group/:user?', - type: 'transaction', - transaction_info: { - source: 'route', - }, - contexts: { - trace: { - data: { - 'url.template': '/group/:group/:user?', - 'url.path': '/', - 'url.full': 'http://localhost:3030/#/group/1/5', - }, - }, - }, - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx index 8cd283650abd..f71572f9dc1f 100644 --- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/src/index.tsx @@ -15,7 +15,6 @@ import User from './pages/User'; 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-create-memory-router/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/spans.test.ts new file mode 100644 index 000000000000..16aef9df1e4e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/spans.test.ts @@ -0,0 +1,70 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '@sentry-internal/test-utils'; + +test('Captures a pageload span', async ({ page }) => { + const spanPromise = waitForStreamedSpan('react-create-memory-router', span => { + return getSpanOp(span) === 'pageload' && span.is_segment; + }); + + await page.goto('/'); + + const span = await spanPromise; + + expect(span.name).toBe('/user/:id'); + expect(span.status).toBe('ok'); + expect(span.span_id).toMatch(/[a-f0-9]{16}/); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); + + expect(span.attributes).toMatchObject({ + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'pageload', type: 'string' }, + 'sentry.origin': { value: 'auto.pageload.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); +}); + +test('Captures a navigation span', async ({ page }) => { + const spansPromise = waitForStreamedSpans('react-create-memory-router', spans => { + return spans.some(span => getSpanOp(span) === 'navigation' && span.is_segment); + }); + + await page.goto('/'); + const linkElement = page.locator('id=navigation-button'); + await linkElement.click(); + + const spans = await spansPromise; + const span = spans.find(span => span.is_segment)!; + + expect(span.name).toBe('/user/:id'); + expect(span.status).toBe('ok'); + expect(span.span_id).toMatch(/[a-f0-9]{16}/); + expect(span.trace_id).toMatch(/[a-f0-9]{32}/); + + expect(span.attributes).toMatchObject({ + 'sentry.idle_span_finish_reason': { value: 'idleTimeout', type: 'string' }, + 'sentry.op': { value: 'navigation', type: 'string' }, + 'sentry.origin': { value: 'auto.navigation.react.reactrouter_v6', type: 'string' }, + 'sentry.sample_rate': { value: 1, type: 'integer' }, + 'sentry.segment.name.source': { value: 'route', type: 'string' }, + 'url.template': { value: '/user/:id', type: 'string' }, + 'url.path': { value: '/', type: 'string' }, + 'url.full': { value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), type: 'string' }, + }); + + expect(span.links).toEqual([ + { + attributes: { + 'sentry.link.type': { value: 'previous_trace', type: 'string' }, + }, + sampled: true, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + ]); + + expect(spans.filter(span => !span.is_segment)).toEqual([]); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts deleted file mode 100644 index 32e2d36cae8c..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-create-memory-router/tests/transactions.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Captures a pageload transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { - return event.contexts?.trace?.op === 'pageload'; - }); - - await page.goto('/'); - - const transactionEvent = await transactionEventPromise; - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/user/:id', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(transactionEvent.contexts?.trace).toEqual( - expect.objectContaining({ - data: expect.objectContaining({ - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'pageload', - 'sentry.origin': 'auto.pageload.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }), - op: 'pageload', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.pageload.react.reactrouter_v6', - status: 'ok', - }), - ); -}); - -test('Captures a navigation transaction', async ({ page }) => { - const transactionEventPromise = waitForTransaction('react-create-memory-router', event => { - return event.contexts?.trace?.op === 'navigation'; - }); - - await page.goto('/'); - const linkElement = page.locator('id=navigation-button'); - await linkElement.click(); - - const transactionEvent = await transactionEventPromise; - expect(transactionEvent.contexts?.trace).toEqual({ - data: expect.objectContaining({ - 'sentry.idle_span_finish_reason': 'idleTimeout', - 'sentry.op': 'navigation', - 'sentry.origin': 'auto.navigation.react.reactrouter_v6', - 'sentry.sample_rate': 1, - 'sentry.segment.name.source': 'route', - 'url.template': '/user/:id', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }), - links: [ - { - attributes: { - 'sentry.link.type': 'previous_trace', - }, - sampled: true, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - }, - ], - op: 'navigation', - span_id: expect.stringMatching(/[a-f0-9]{16}/), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.navigation.react.reactrouter_v6', - status: 'ok', - }); - - expect(transactionEvent).toEqual( - expect.objectContaining({ - transaction: '/user/:id', - type: 'transaction', - transaction_info: { - source: 'route', - }, - }), - ); - - expect(transactionEvent.spans).toEqual([]); -}); 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' }, }); });