diff --git a/dev-packages/e2e-tests/test-applications/angular-19/angular.json b/dev-packages/e2e-tests/test-applications/angular-19/angular.json index 355946a85d15..f81a2f56cc8f 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/angular.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/angular.json @@ -47,6 +47,14 @@ "optimization": false, "extractLicenses": false, "sourceMap": true + }, + "streamed": { + "fileReplacements": [ + { + "replace": "src/trace-lifecycle.ts", + "with": "src/trace-lifecycle.streamed.ts" + } + ] } }, "defaultConfiguration": "production" diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json index 2b6c81fb7a20..bc7f7ca45433 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/package.json +++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json @@ -11,7 +11,10 @@ "test": "playwright test", "test:build": "pnpm install && pnpm build", "test:assert": "playwright test", - "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist" + "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist", + "build:streamed": "ng build --configuration production,streamed", + "test:build:streamed": "pnpm install && pnpm build:streamed", + "test:assert:streamed": "E2E_TEST_TRACE_LIFECYCLE=stream playwright test" }, "private": true, "dependencies": { @@ -47,5 +50,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "label": "angular-19 (streamed)", + "build-command": "pnpm test:build:streamed", + "assert-command": "pnpm test:assert:streamed" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/main.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/main.ts index d41666c20925..94b1c7786550 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/src/main.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/src/main.ts @@ -1,11 +1,12 @@ import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; import { appConfig } from './app/app.config'; +import { TRACE_LIFECYCLE } from './trace-lifecycle'; import * as Sentry from '@sentry/angular'; Sentry.init({ - traceLifecycle: 'static', + traceLifecycle: TRACE_LIFECYCLE, // Cannot use process.env here, so we hardcode the DSN dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.streamed.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.streamed.ts new file mode 100644 index 000000000000..8c6daf7d7bcf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.streamed.ts @@ -0,0 +1,2 @@ +// Replaces `trace-lifecycle.ts` in the `streamed` build configuration. See `angular.json`. +export const TRACE_LIFECYCLE = 'stream' as 'static' | 'stream'; diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.ts new file mode 100644 index 000000000000..7f4ebfc1b740 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-19/src/trace-lifecycle.ts @@ -0,0 +1,3 @@ +// Swapped for `trace-lifecycle.streamed.ts` by the `streamed` build configuration in `angular.json`. +// Angular can't read `process.env` in the browser bundle, so the lifecycle is picked at build time. +export const TRACE_LIFECYCLE = 'static' as 'static' | 'stream'; diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts index 7fa706c00504..44e8e8b668b5 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts @@ -30,6 +30,9 @@ test('sends an error', async ({ page }) => { }); test('assigns the correct transaction value after a navigation', async ({ page }) => { + // Waits on a pageload transaction, which the streamed variant never emits. + test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE === 'stream', 'transactions are not emitted with span streaming'); + const pageloadTxnPromise = waitForTransaction('angular-19', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; }); diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts index 35a2bf09a45f..893b364ca082 100644 --- a/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts @@ -3,6 +3,10 @@ import { waitForTransaction } from '@sentry-internal/test-utils'; // Cannot use @sentry/angular here due to build stuff import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +// The `angular-19 (streamed)` variant builds the app with `traceLifecycle: 'stream'`, which emits +// spans instead of transactions. See `streamed-performance.test.ts` for that variant. +test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE === 'stream', 'transactions are not emitted with span streaming'); + test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('angular-19', async transactionEvent => { return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tests/streamed-performance.test.ts b/dev-packages/e2e-tests/test-applications/angular-19/tests/streamed-performance.test.ts new file mode 100644 index 000000000000..97e01a9194ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/streamed-performance.test.ts @@ -0,0 +1,20 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +// Only the `angular-19 (streamed)` variant builds the app with `traceLifecycle: 'stream'`. +test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE !== 'stream', 'requires the app built with span streaming'); + +test('names the routing span with the low cardinality fallback', async ({ page }) => { + const routingSpanPromise = waitForStreamedSpan('angular-19', span => getSpanOp(span) === 'router'); + + await page.goto('/'); + await page.locator('#navLink').click(); + + const routingSpan = await routingSpanPromise; + + // The routing span starts at `NavigationStart`, where only the raw URL (`/users/123`) is known. + // Angular resolves the parameterized route at `ResolveEnd` and applies it to the root span, so a + // streamed routing span has nothing low cardinality to use and takes the static fallback. + expect(routingSpan.name).toBe('Router'); + expect(routingSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ui.angular' }); +}); diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json index 3502d5ab801c..1d704feb186a 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/package.json @@ -9,7 +9,9 @@ "start": "vite preview --port 3030", "test": "playwright test", "test:build": "pnpm install && pnpm run build", - "test:assert": "pnpm run test" + "test:assert": "pnpm run test", + "test:build:streamed": "VITE_TRACE_LIFECYCLE=stream pnpm run test:build", + "test:assert:streamed": "E2E_TEST_TRACE_LIFECYCLE=stream pnpm run test" }, "devDependencies": { "@playwright/test": "~1.56.0", @@ -42,5 +44,14 @@ }, "volta": { "extends": "../../package.json" + }, + "sentryTest": { + "variants": [ + { + "label": "ember-strict-resolver (streamed)", + "build-command": "pnpm test:build:streamed", + "assert-command": "pnpm test:assert:streamed" + } + ] } } diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/src/app.gts b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/src/app.gts index 0f20c8034fb3..ec3b3da00b56 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/src/app.gts +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/src/app.gts @@ -6,7 +6,9 @@ import * as Sentry from '@sentry/ember'; // Initialize Sentry Sentry.init({ dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', - traceLifecycle: 'static', + // Set by the `ember-strict-resolver (streamed)` test variant, which rebuilds the app to cover + // span streaming. Vite inlines this at build time, so it can't be flipped at runtime. + traceLifecycle: import.meta.env.VITE_TRACE_LIFECYCLE === 'stream' ? 'stream' : 'static', tracesSampleRate: 1.0, tunnel: 'http://localhost:3031/', // proxy server }); diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/errors.test.ts index d003aec0c290..2539aef94e97 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/errors.test.ts @@ -30,6 +30,9 @@ test('sends an error', async ({ page }) => { }); test('assigns the correct transaction value after a navigation', async ({ page }) => { + // Waits on a pageload transaction, which the streamed variant never emits. + test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE === 'stream', 'transactions are not emitted with span streaming'); + const pageloadTxnPromise = waitForTransaction('ember-strict-resolver', async transactionEvent => { return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; }); diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/sentry-performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/sentry-performance.test.ts index a917bb72c3b0..b84678df0397 100644 --- a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/sentry-performance.test.ts +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/sentry-performance.test.ts @@ -1,6 +1,10 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; +// The `ember-strict-resolver (streamed)` variant builds the app with `traceLifecycle: 'stream'`, +// which emits spans instead of transactions. See `streamed-performance.test.ts` for that variant. +test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE === 'stream', 'transactions are not emitted with span streaming'); + test('sends a pageload transaction with a parameterized URL', async ({ page }) => { const transactionPromise = waitForTransaction('ember-strict-resolver', async transactionEvent => { return !!transactionEvent.transaction && transactionEvent.contexts?.trace?.op === 'pageload'; diff --git a/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/streamed-performance.test.ts b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/streamed-performance.test.ts new file mode 100644 index 000000000000..bd6405989474 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/ember-strict-resolver/tests/streamed-performance.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForStreamedSpan } from '@sentry-internal/test-utils'; + +// Only the `ember-strict-resolver (streamed)` variant builds the app with `traceLifecycle: 'stream'`. +test.skip(process.env.E2E_TEST_TRACE_LIFECYCLE !== 'stream', 'requires the app built with span streaming'); + +test('names the transition span with the low cardinality fallback', async ({ page }) => { + const transitionSpanPromise = waitForStreamedSpan('ember-strict-resolver', span => getSpanOp(span) === 'router'); + + await page.goto('/'); + await page.getByText('Tracing').click(); + + const transitionSpan = await transitionSpanPromise; + + // The route pair (`route:index -> route:tracing`) is not one of the convention's name templates, + // so a streamed router span takes the static fallback instead. + expect(transitionSpan.name).toBe('Router'); + expect(transitionSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.ui.ember' }); +});