diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts b/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts index e03c77ddc1a4..5835dac94c19 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/app/app.ts @@ -7,7 +7,6 @@ import config from './config/environment'; Sentry.init({ dsn: config.sentryDsn, - traceLifecycle: 'static', tracesSampleRate: 1, replaysSessionSampleRate: 1, replaysOnErrorSampleRate: 1, diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts index a34194d4fd30..7477699aa444 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/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 an error', async ({ page }) => { const errorPromise = waitForError('ember-classic', async errorEvent => { @@ -30,8 +30,8 @@ test('sends an error', async ({ page }) => { }); test('assigns the correct transaction value after a navigation', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); const errorPromise = waitForError('ember-classic', async errorEvent => { @@ -39,7 +39,7 @@ test('assigns the correct transaction value after a navigation', async ({ page } }); await page.goto(`/tracing`); - await pageloadTxnPromise; + await pageloadSpanPromise; await page.getByText('Errors').click(); diff --git a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts index 141eb8cec968..5875a4483ae1 100644 --- a/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-classic/tests/performance.test.ts @@ -1,327 +1,245 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); await page.goto(`/`); - const rootSpan = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - data: { - 'sentry.origin': 'auto.pageload.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/' }, + 'url.path': { type: 'string', value: '/' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/) }, }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const [_, navigationTxn] = await Promise.all([page.getByText('Tracing').click(), navigationTxnPromise]); + const [_, navigationSpan] = await Promise.all([page.getByText('Tracing').click(), navigationSpanPromise]); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/tracing', - 'url.path': '/tracing', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/), - }, - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/tracing' }, + 'url.path': { type: 'string', value: '/tracing' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/) }, }, }); }); -test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span even if the pageload span is still active', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); // immediately navigate to a different route - const [_, pageloadTxn, navigationTxn] = await Promise.all([ + const [_, pageloadSpan, navigationSpan] = await Promise.all([ page.getByText('Tracing').click(), - pageloadTxnPromise, - navigationTxnPromise, + pageloadSpanPromise, + navigationSpanPromise, ]); - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - data: { - 'sentry.origin': 'auto.pageload.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/' }, + 'url.path': { type: 'string', value: '/' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/) }, }, }); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/tracing', - 'url.path': '/tracing', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/), - }, - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/tracing' }, + 'url.path': { type: 'string', value: '/tracing' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/) }, }, }); }); test('captures correct spans for navigation', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('ember-classic', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-classic', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const spansPromise = collectStreamedSpans('ember-classic', spans => { + const navigationSpan = spans.find( + span => span.is_segment && getSpanOp(span) === 'navigation' && span.name === 'route:slow-loading-route.index', + ); + + return ( + !!navigationSpan && + spans.some(span => getSpanOp(span) === 'router' && span.parent_span_id === navigationSpan.span_id) && + spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'beforeModel', + ).length >= 2 && + spans.filter(span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'model') + .length >= 2 && + spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'afterModel', + ).length >= 2 && + spans.filter(span => getSpanOp(span) === 'ui.task' && span.attributes['ember.runloop.queue']?.value === 'render') + .length > 1 + ); }); await page.goto(`/tracing`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const [_, navigationTxn] = await Promise.all([page.getByText('Measure Things!').click(), navigationTxnPromise]); + const [_, spans] = await Promise.all([page.getByText('Measure Things!').click(), spansPromise]); - const traceId = navigationTxn.contexts?.trace?.trace_id; - const spanId = navigationTxn.contexts?.trace?.span_id; - - expect(traceId).toBeDefined(); - expect(spanId).toBeDefined(); - - const spans = navigationTxn.spans || []; + const navigationSpan = spans.find( + span => span.is_segment && getSpanOp(span) === 'navigation' && span.name === 'route:slow-loading-route.index', + ); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/slow-loading-route', - 'url.path': '/slow-loading-route', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/), - }, - }, - }, - transaction: 'route:slow-loading-route.index', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:slow-loading-route.index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/slow-loading-route' }, + 'url.path': { type: 'string', value: '/slow-loading-route' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/) }, }, }); - const transitionSpans = spans.filter(span => span.op === 'router'); + const transitionSpans = spans.filter( + span => getSpanOp(span) === 'router' && span.parent_span_id === navigationSpan?.span_id, + ); const beforeModelSpans = spans.filter( - span => span.op === 'function' && span.data?.['code.function.name'] === 'beforeModel', + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'beforeModel', + ); + const modelSpans = spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'model', ); - const modelSpans = spans.filter(span => span.op === 'function' && span.data?.['code.function.name'] === 'model'); const afterModelSpans = spans.filter( - span => span.op === 'function' && span.data?.['code.function.name'] === 'afterModel', + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'afterModel', + ); + const renderSpans = spans.filter( + span => getSpanOp(span) === 'ui.task' && span.attributes['ember.runloop.queue']?.value === 'render', ); - const renderSpans = spans.filter(span => span.op === 'ui.task' && span.data?.['ember.runloop.queue'] === 'render'); expect(transitionSpans).toHaveLength(1); - - // We have two spans each there - one for `slow-loading-route` and one for `slow-load-route.index` expect(beforeModelSpans).toHaveLength(2); expect(modelSpans).toHaveLength(2); expect(afterModelSpans).toHaveLength(2); - - // There may be many render spans... expect(renderSpans.length).toBeGreaterThan(1); - expect(transitionSpans[0]).toEqual({ - data: { - 'sentry.op': 'router', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'route:tracing -> route:slow-loading-route.index', - op: 'router', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, + expect(transitionSpans[0]).toMatchObject({ + name: 'Router', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'router' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), }); - expect(beforeModelSpans).toEqual([ - { - data: { - 'code.function.name': 'beforeModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'beforeModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(beforeModelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'beforeModel' }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'beforeModel' }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + ]), + ); - expect(modelSpans).toEqual([ - { - data: { - 'code.function.name': 'model', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'model', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(modelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'model' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'model' }, + }), + }), + ]), + ); - expect(afterModelSpans).toEqual([ - { - data: { - 'code.function.name': 'afterModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'afterModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(afterModelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'afterModel' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'afterModel' }, + }), + }), + ]), + ); - expect(renderSpans).toContainEqual({ - data: { - 'ember.runloop.queue': 'render', - 'sentry.op': 'ui.task', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'runloop', - op: 'ui.task', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(renderSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'runloop', + attributes: expect.objectContaining({ + 'ember.runloop.queue': { type: 'string', value: 'render' }, + 'sentry.op': { type: 'string', value: 'ui.task' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + ]), + ); }); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts index 2d4331aacf83..85150a4a135a 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/app/app.ts @@ -6,7 +6,6 @@ import Resolver from 'ember-resolver'; Sentry.init({ dsn: config.sentryDsn, - traceLifecycle: 'static', tracesSampleRate: 1, replaysSessionSampleRate: 1, replaysOnErrorSampleRate: 1, diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts index 50ec82e11153..9feede130be8 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/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 an error', async ({ page }) => { const errorPromise = waitForError('ember-embroider', async errorEvent => { @@ -30,8 +30,8 @@ test('sends an error', async ({ page }) => { }); test('assigns the correct transaction value after a navigation', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); const errorPromise = waitForError('ember-embroider', async errorEvent => { @@ -39,7 +39,7 @@ test('assigns the correct transaction value after a navigation', async ({ page } }); await page.goto(`/tracing`); - await pageloadTxnPromise; + await pageloadSpanPromise; await page.getByText('Errors').click(); diff --git a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts index 4043b52dc04b..4de378600e6c 100644 --- a/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-embroider/tests/performance.test.ts @@ -1,327 +1,245 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); await page.goto(`/`); - const rootSpan = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - data: { - 'sentry.origin': 'auto.pageload.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/' }, + 'url.path': { type: 'string', value: '/' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/) }, }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const [_, navigationTxn] = await Promise.all([page.getByText('Tracing').click(), navigationTxnPromise]); + const [_, navigationSpan] = await Promise.all([page.getByText('Tracing').click(), navigationSpanPromise]); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/tracing', - 'url.path': '/tracing', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/), - }, - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/tracing' }, + 'url.path': { type: 'string', value: '/tracing' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/) }, }, }); }); -test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span even if the pageload span is still active', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); // immediately navigate to a different route - const [_, pageloadTxn, navigationTxn] = await Promise.all([ + const [_, pageloadSpan, navigationSpan] = await Promise.all([ page.getByText('Tracing').click(), - pageloadTxnPromise, - navigationTxnPromise, + pageloadSpanPromise, + navigationSpanPromise, ]); - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - data: { - 'sentry.origin': 'auto.pageload.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/', - 'url.path': '/', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/$/), - }, - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/' }, + 'url.path': { type: 'string', value: '/' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/$/) }, }, }); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/tracing', - 'url.path': '/tracing', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/), - }, - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/tracing' }, + 'url.path': { type: 'string', value: '/tracing' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/tracing$/) }, }, }); }); test('captures correct spans for navigation', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('ember-embroider', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-embroider', async transactionEvent => { - return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const spansPromise = collectStreamedSpans('ember-embroider', spans => { + const navigationSpan = spans.find( + span => span.is_segment && getSpanOp(span) === 'navigation' && span.name === 'route:slow-loading-route.index', + ); + + return ( + !!navigationSpan && + spans.some(span => getSpanOp(span) === 'router' && span.parent_span_id === navigationSpan.span_id) && + spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'beforeModel', + ).length >= 2 && + spans.filter(span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'model') + .length >= 2 && + spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'afterModel', + ).length >= 2 && + spans.filter(span => getSpanOp(span) === 'ui.task' && span.attributes['ember.runloop.queue']?.value === 'render') + .length > 1 + ); }); await page.goto(`/tracing`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const [_, navigationTxn] = await Promise.all([page.getByText('Measure Things!').click(), navigationTxnPromise]); + const [_, spans] = await Promise.all([page.getByText('Measure Things!').click(), spansPromise]); - const traceId = navigationTxn.contexts?.trace?.trace_id; - const spanId = navigationTxn.contexts?.trace?.span_id; - - expect(traceId).toBeDefined(); - expect(spanId).toBeDefined(); - - const spans = navigationTxn.spans || []; + const navigationSpan = spans.find( + span => span.is_segment && getSpanOp(span) === 'navigation' && span.name === 'route:slow-loading-route.index', + ); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - data: { - 'sentry.origin': 'auto.navigation.ember', - 'sentry.segment.name.source': 'route', - 'url.template': '/slow-loading-route', - 'url.path': '/slow-loading-route', - 'url.full': expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/), - }, - }, - }, - transaction: 'route:slow-loading-route.index', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:slow-loading-route.index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, + 'sentry.segment.name.source': { type: 'string', value: 'route' }, + 'url.template': { type: 'string', value: '/slow-loading-route' }, + 'url.path': { type: 'string', value: '/slow-loading-route' }, + 'url.full': { type: 'string', value: expect.stringMatching(/^https?:\/\/localhost:\d+\/slow-loading-route$/) }, }, }); - const transitionSpans = spans.filter(span => span.op === 'router'); + const transitionSpans = spans.filter( + span => getSpanOp(span) === 'router' && span.parent_span_id === navigationSpan?.span_id, + ); const beforeModelSpans = spans.filter( - span => span.op === 'function' && span.data?.['code.function.name'] === 'beforeModel', + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'beforeModel', + ); + const modelSpans = spans.filter( + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'model', ); - const modelSpans = spans.filter(span => span.op === 'function' && span.data?.['code.function.name'] === 'model'); const afterModelSpans = spans.filter( - span => span.op === 'function' && span.data?.['code.function.name'] === 'afterModel', + span => getSpanOp(span) === 'function' && span.attributes['code.function.name']?.value === 'afterModel', + ); + const renderSpans = spans.filter( + span => getSpanOp(span) === 'ui.task' && span.attributes['ember.runloop.queue']?.value === 'render', ); - const renderSpans = spans.filter(span => span.op === 'ui.task' && span.data?.['ember.runloop.queue'] === 'render'); expect(transitionSpans).toHaveLength(1); - - // We have two spans each there - one for `slow-loading-route` and one for `slow-load-route.index` expect(beforeModelSpans).toHaveLength(2); expect(modelSpans).toHaveLength(2); expect(afterModelSpans).toHaveLength(2); - - // There may be many render spans... expect(renderSpans.length).toBeGreaterThan(1); - expect(transitionSpans[0]).toEqual({ - data: { - 'sentry.op': 'router', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'route:tracing -> route:slow-loading-route.index', - op: 'router', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, + expect(transitionSpans[0]).toMatchObject({ + name: 'Router', + attributes: expect.objectContaining({ + 'sentry.op': { type: 'string', value: 'router' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), }); - expect(beforeModelSpans).toEqual([ - { - data: { - 'code.function.name': 'beforeModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'beforeModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(beforeModelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'beforeModel' }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'beforeModel' }, + 'sentry.op': { type: 'string', value: 'function' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + ]), + ); - expect(modelSpans).toEqual([ - { - data: { - 'code.function.name': 'model', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'model', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(modelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'model' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'model' }, + }), + }), + ]), + ); - expect(afterModelSpans).toEqual([ - { - data: { - 'code.function.name': 'afterModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - { - data: { - 'code.function.name': 'afterModel', - 'sentry.op': 'function', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'slow-loading-route.index', - op: 'function', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }, - ]); + expect(afterModelSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'slow-loading-route', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'afterModel' }, + }), + }), + expect.objectContaining({ + name: 'slow-loading-route.index', + attributes: expect.objectContaining({ + 'code.function.name': { type: 'string', value: 'afterModel' }, + }), + }), + ]), + ); - expect(renderSpans).toContainEqual({ - data: { - 'ember.runloop.queue': 'render', - 'sentry.op': 'ui.task', - 'sentry.origin': 'auto.ui.ember', - }, - description: 'runloop', - op: 'ui.task', - origin: 'auto.ui.ember', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/[a-f0-9]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(renderSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: 'runloop', + attributes: expect.objectContaining({ + 'ember.runloop.queue': { type: 'string', value: 'render' }, + 'sentry.op': { type: 'string', value: 'ui.task' }, + 'sentry.origin': { type: 'string', value: 'auto.ui.ember' }, + }), + }), + ]), + ); }); diff --git a/dev-packages/e2e-tests/test-applications/ember-vite/app/app.ts b/dev-packages/e2e-tests/test-applications/ember-vite/app/app.ts index 569e40fdfa5f..0e288acb1b18 100644 --- a/dev-packages/e2e-tests/test-applications/ember-vite/app/app.ts +++ b/dev-packages/e2e-tests/test-applications/ember-vite/app/app.ts @@ -9,7 +9,6 @@ import config from 'ember-vite/config/environment'; Sentry.init({ dsn: config.sentryDsn, - traceLifecycle: 'static', tracesSampleRate: 1, replaysSessionSampleRate: 1, replaysOnErrorSampleRate: 1, diff --git a/dev-packages/e2e-tests/test-applications/ember-vite/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-vite/tests/errors.test.ts index 94ac92fbb924..aba715c4e6ce 100644 --- a/dev-packages/e2e-tests/test-applications/ember-vite/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-vite/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 an error', async ({ page }) => { const errorPromise = waitForError('ember-vite', async errorEvent => { @@ -28,8 +28,8 @@ test('sends an error', async ({ page }) => { }); test('assigns the correct transaction value after a navigation', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; + const pageloadSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); const errorPromise = waitForError('ember-vite', async errorEvent => { @@ -37,7 +37,7 @@ test('assigns the correct transaction value after a navigation', async ({ page } }); await page.goto(`/tracing`); - await pageloadTxnPromise; + await pageloadSpanPromise; await page.getByText('Errors').click(); diff --git a/dev-packages/e2e-tests/test-applications/ember-vite/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-vite/tests/performance.test.ts index 7fdf59b01fbd..513cdb2fd11f 100644 --- a/dev-packages/e2e-tests/test-applications/ember-vite/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-vite/tests/performance.test.ts @@ -1,98 +1,82 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; -test('sends a pageload transaction with a parameterized URL', async ({ page }) => { - const transactionPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a pageload span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); await page.goto(`/`); - const rootSpan = await transactionPromise; + const pageloadSpan = await pageloadSpanPromise; - expect(rootSpan).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, }, }); }); -test('sends a navigation transaction with a parameterized URL', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span with a parameterized URL', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); - await pageloadTxnPromise; + await pageloadSpanPromise; - const [_, navigationTxn] = await Promise.all([page.getByText('Tracing').click(), navigationTxnPromise]); + const [_, navigationSpan] = await Promise.all([page.getByText('Tracing').click(), navigationSpanPromise]); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, }, }); }); -test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => { - const pageloadTxnPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; +test('sends a navigation span even if the pageload span is still active', async ({ page }) => { + const pageloadSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'pageload'; }); - const navigationTxnPromise = waitForTransaction('ember-vite', async transactionEvent => { - return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation'; + const navigationSpanPromise = waitForStreamedSpan('ember-vite', span => { + return span.is_segment && getSpanOp(span) === 'navigation'; }); await page.goto(`/`); // immediately navigate to a different route - const [_, pageloadTxn, navigationTxn] = await Promise.all([ + const [_, pageloadSpan, navigationSpan] = await Promise.all([ page.getByText('Tracing').click(), - pageloadTxnPromise, - navigationTxnPromise, + pageloadSpanPromise, + navigationSpanPromise, ]); - expect(pageloadTxn).toMatchObject({ - contexts: { - trace: { - op: 'pageload', - origin: 'auto.pageload.ember', - }, - }, - transaction: 'route:index', - transaction_info: { - source: 'route', + expect(pageloadSpan).toMatchObject({ + name: 'route:index', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'pageload' }, + 'sentry.origin': { type: 'string', value: 'auto.pageload.ember' }, }, }); - expect(navigationTxn).toMatchObject({ - contexts: { - trace: { - op: 'navigation', - origin: 'auto.navigation.ember', - }, - }, - transaction: 'route:tracing', - transaction_info: { - source: 'route', + expect(navigationSpan).toMatchObject({ + name: 'route:tracing', + is_segment: true, + attributes: { + 'sentry.op': { type: 'string', value: 'navigation' }, + 'sentry.origin': { type: 'string', value: 'auto.navigation.ember' }, }, }); });