diff --git a/dev-packages/e2e-tests/test-applications/angular-19/.editorconfig b/dev-packages/e2e-tests/test-applications/angular-19/.editorconfig
new file mode 100644
index 000000000000..f166060da1cb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/.editorconfig
@@ -0,0 +1,17 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+ij_typescript_use_double_quotes = false
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/.gitignore b/dev-packages/e2e-tests/test-applications/angular-19/.gitignore
new file mode 100644
index 000000000000..315c644a53e8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/.gitignore
@@ -0,0 +1,44 @@
+# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
+
+test-results
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/README.md b/dev-packages/e2e-tests/test-applications/angular-19/README.md
new file mode 100644
index 000000000000..e971396e817c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/README.md
@@ -0,0 +1,3 @@
+# Angular 19
+
+E2E test app for Angular 19 and `@sentry/angular`.
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/angular.json b/dev-packages/e2e-tests/test-applications/angular-19/angular.json
new file mode 100644
index 000000000000..446b28df1d25
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/angular.json
@@ -0,0 +1,96 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "angular-19": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:application",
+ "options": {
+ "outputPath": "dist/angular-19",
+ "index": "src/index.html",
+ "browser": "src/main.ts",
+ "polyfills": [
+ "zone.js"
+ ],
+ "tsConfig": "tsconfig.app.json",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "public"
+ }
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kB",
+ "maximumError": "8kB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "angular-19:build:production"
+ },
+ "development": {
+ "buildTarget": "angular-19:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n"
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ],
+ "tsConfig": "tsconfig.spec.json",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "public"
+ }
+ ],
+ "styles": [
+ "src/styles.css"
+ ],
+ "scripts": []
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/package.json b/dev-packages/e2e-tests/test-applications/angular-19/package.json
new file mode 100644
index 000000000000..88b4334edbff
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "angular-19",
+ "version": "0.0.0",
+ "scripts": {
+ "ng": "ng",
+ "dev": "ng serve",
+ "proxy": "node start-event-proxy.mjs",
+ "preview": "http-server dist/angular-19/browser --port 8080",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "playwright test",
+ "test:build": "pnpm install && npx playwright install && pnpm build",
+ "test:assert": "playwright test",
+ "clean": "npx rimraf .angular node_modules pnpm-lock.yaml dist"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "^19.0.0",
+ "@angular/common": "^19.0.0",
+ "@angular/compiler": "^19.0.0",
+ "@angular/core": "^19.0.0",
+ "@angular/forms": "^19.0.0",
+ "@angular/platform-browser": "^19.0.0",
+ "@angular/platform-browser-dynamic": "^19.0.0",
+ "@angular/router": "^19.0.0",
+ "@sentry/angular": "* || latest",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.15.0"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "^19.0.0",
+ "@angular/cli": "^19.0.0",
+ "@angular/compiler-cli": "^19.0.0",
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@types/jasmine": "~5.1.0",
+ "http-server": "^14.1.1",
+ "jasmine-core": "~5.4.0",
+ "karma": "~6.4.0",
+ "karma-chrome-launcher": "~3.2.0",
+ "karma-coverage": "~2.2.0",
+ "karma-jasmine": "~5.1.0",
+ "karma-jasmine-html-reporter": "~2.1.0",
+ "typescript": "~5.6.2"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/angular-19/playwright.config.mjs
new file mode 100644
index 000000000000..0845325879c9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/playwright.config.mjs
@@ -0,0 +1,8 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+const config = getPlaywrightConfig({
+ startCommand: `pnpm preview`,
+ port: 8080,
+});
+
+export default config;
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/public/favicon.ico b/dev-packages/e2e-tests/test-applications/angular-19/public/favicon.ico
new file mode 100644
index 000000000000..57614f9c9675
Binary files /dev/null and b/dev-packages/e2e-tests/test-applications/angular-19/public/favicon.ico differ
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.component.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.component.ts
new file mode 100644
index 000000000000..b79fcfcf453c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.component.ts
@@ -0,0 +1,12 @@
+import { Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ selector: 'app-root',
+ standalone: true,
+ imports: [RouterOutlet],
+ template: ``,
+})
+export class AppComponent {
+ title = 'angular-19';
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.config.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.config.ts
new file mode 100644
index 000000000000..f5cc30f3615b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.config.ts
@@ -0,0 +1,29 @@
+import {
+ ApplicationConfig,
+ ErrorHandler,
+ inject,
+ provideAppInitializer,
+ provideZoneChangeDetection,
+} from '@angular/core';
+import { Router, provideRouter } from '@angular/router';
+
+import { TraceService, createErrorHandler } from '@sentry/angular';
+import { routes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideRouter(routes),
+ {
+ provide: ErrorHandler,
+ useValue: createErrorHandler(),
+ },
+ {
+ provide: TraceService,
+ deps: [Router],
+ },
+ provideAppInitializer(() => {
+ inject(TraceService);
+ }),
+ ],
+};
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.routes.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.routes.ts
new file mode 100644
index 000000000000..24bf8b769051
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/app.routes.ts
@@ -0,0 +1,42 @@
+import { Routes } from '@angular/router';
+import { cancelGuard } from './cancel-guard.guard';
+import { CancelComponent } from './cancel/cancel.components';
+import { ComponentTrackingComponent } from './component-tracking/component-tracking.components';
+import { HomeComponent } from './home/home.component';
+import { UserComponent } from './user/user.component';
+
+export const routes: Routes = [
+ {
+ path: 'users/:id',
+ component: UserComponent,
+ },
+ {
+ path: 'home',
+ component: HomeComponent,
+ },
+ {
+ path: 'cancel',
+ component: CancelComponent,
+ canActivate: [cancelGuard],
+ },
+ {
+ path: 'component-tracking',
+ component: ComponentTrackingComponent,
+ },
+ {
+ path: 'redirect1',
+ redirectTo: '/redirect2',
+ },
+ {
+ path: 'redirect2',
+ redirectTo: '/redirect3',
+ },
+ {
+ path: 'redirect3',
+ redirectTo: '/users/456',
+ },
+ {
+ path: '**',
+ redirectTo: 'home',
+ },
+];
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel-guard.guard.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel-guard.guard.ts
new file mode 100644
index 000000000000..16ec4a2ab164
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel-guard.guard.ts
@@ -0,0 +1,5 @@
+import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router';
+
+export const cancelGuard: CanActivateFn = (_next: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
+ return false;
+};
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel/cancel.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel/cancel.components.ts
new file mode 100644
index 000000000000..b6ee1876e035
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/cancel/cancel.components.ts
@@ -0,0 +1,8 @@
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-cancel',
+ standalone: true,
+ template: `
`,
+})
+export class CancelComponent {}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts
new file mode 100644
index 000000000000..d437a1d43fdd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/component-tracking/component-tracking.components.ts
@@ -0,0 +1,18 @@
+import { AfterViewInit, Component, OnInit } from '@angular/core';
+import { TraceClass, TraceMethod, TraceModule } from '@sentry/angular';
+import { SampleComponent } from '../sample-component/sample-component.components';
+
+@Component({
+ selector: 'app-cancel',
+ standalone: true,
+ imports: [TraceModule, SampleComponent],
+ template: ``,
+})
+@TraceClass({ name: 'ComponentTrackingComponent' })
+export class ComponentTrackingComponent implements OnInit, AfterViewInit {
+ @TraceMethod({ name: 'ngOnInit' })
+ ngOnInit() {}
+
+ @TraceMethod()
+ ngAfterViewInit() {}
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/home/home.component.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/home/home.component.ts
new file mode 100644
index 000000000000..fe70dabd687a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/home/home.component.ts
@@ -0,0 +1,26 @@
+import { Component } from '@angular/core';
+import { RouterLink } from '@angular/router';
+
+@Component({
+ selector: 'app-home',
+ standalone: true,
+ imports: [RouterLink],
+ template: `
+
+ Welcome to Sentry's Angular 19 E2E test app
+
+
+
+`,
+})
+export class HomeComponent {
+ throwError() {
+ throw new Error('Error thrown from Angular 18 E2E test app');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/sample-component/sample-component.components.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/sample-component/sample-component.components.ts
new file mode 100644
index 000000000000..da09425c7565
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/sample-component/sample-component.components.ts
@@ -0,0 +1,12 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-sample-component',
+ standalone: true,
+ template: `Component
`,
+})
+export class SampleComponent implements OnInit {
+ ngOnInit() {
+ console.log('SampleComponent');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/app/user/user.component.ts b/dev-packages/e2e-tests/test-applications/angular-19/src/app/user/user.component.ts
new file mode 100644
index 000000000000..db02568d395f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/app/user/user.component.ts
@@ -0,0 +1,25 @@
+import { AsyncPipe } from '@angular/common';
+import { Component } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Observable, map } from 'rxjs';
+
+@Component({
+ selector: 'app-user',
+ standalone: true,
+ imports: [AsyncPipe],
+ template: `
+ Hello User {{ userId$ | async }}
+
+ `,
+})
+export class UserComponent {
+ public userId$: Observable;
+
+ constructor(private route: ActivatedRoute) {
+ this.userId$ = this.route.paramMap.pipe(map(params => params.get('id') || 'UNKNOWN USER'));
+ }
+
+ throwError() {
+ throw new Error('Error thrown from user page');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/index.html b/dev-packages/e2e-tests/test-applications/angular-19/src/index.html
new file mode 100644
index 000000000000..a0fab84284d8
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Angular19
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000000..a0b841afc333
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/main.ts
@@ -0,0 +1,15 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { AppComponent } from './app/app.component';
+import { appConfig } from './app/app.config';
+
+import * as Sentry from '@sentry/angular';
+
+Sentry.init({
+ // Cannot use process.env here, so we hardcode the DSN
+ dsn: 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576',
+ tracesSampleRate: 1.0,
+ integrations: [Sentry.browserTracingIntegration({})],
+ tunnel: `http://localhost:3031/`, // proxy server
+});
+
+bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err));
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/src/styles.css b/dev-packages/e2e-tests/test-applications/angular-19/src/styles.css
new file mode 100644
index 000000000000..90d4ee0072ce
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/src/styles.css
@@ -0,0 +1 @@
+/* You can add global styles to this file, and also import other style files */
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs
new file mode 100644
index 000000000000..b1b4620866bd
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'angular-18',
+});
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
new file mode 100644
index 000000000000..36d23bd077a5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/errors.test.ts
@@ -0,0 +1,65 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('sends an error', async ({ page }) => {
+ const errorPromise = waitForError('angular-18', async errorEvent => {
+ return !errorEvent.type;
+ });
+
+ await page.goto(`/`);
+
+ await page.locator('#errorBtn').click();
+
+ const error = await errorPromise;
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from Angular 18 E2E test app',
+ mechanism: {
+ type: 'angular',
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/home/',
+ });
+});
+
+test('assigns the correct transaction value after a navigation', async ({ page }) => {
+ const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const errorPromise = waitForError('angular-18', async errorEvent => {
+ return !errorEvent.type;
+ });
+
+ await page.goto(`/`);
+ await pageloadTxnPromise;
+
+ await page.waitForTimeout(5000);
+
+ await page.locator('#navLink').click();
+
+ const [_, error] = await Promise.all([page.locator('#userErrorBtn').click(), errorPromise]);
+
+ expect(error).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Error thrown from user page',
+ mechanism: {
+ type: 'angular',
+ handled: false,
+ },
+ },
+ ],
+ },
+ transaction: '/users/:id/',
+ });
+});
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
new file mode 100644
index 000000000000..af85b8ffc405
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tests/performance.test.ts
@@ -0,0 +1,313 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
+
+test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
+ const transactionPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ await page.goto(`/`);
+
+ const rootSpan = await transactionPromise;
+
+ expect(rootSpan).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.angular',
+ },
+ },
+ transaction: '/home/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
+test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
+ const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+ await pageloadTxnPromise;
+
+ await page.waitForTimeout(5000);
+
+ const [_, navigationTxn] = await Promise.all([page.locator('#navLink').click(), navigationTxnPromise]);
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ },
+ },
+ transaction: '/users/:id/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
+test('sends a navigation transaction even if the pageload span is still active', async ({ page }) => {
+ const pageloadTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
+ });
+
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, pageloadTxn, navigationTxn] = await Promise.all([
+ page.locator('#navLink').click(),
+ pageloadTxnPromise,
+ navigationTxnPromise,
+ ]);
+
+ expect(pageloadTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'pageload',
+ origin: 'auto.pageload.angular',
+ },
+ },
+ transaction: '/home/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.angular',
+ },
+ },
+ transaction: '/users/:id/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+});
+
+test('groups redirects within one navigation root span', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#redirectLink').click(), navigationTxnPromise]);
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.angular',
+ },
+ },
+ transaction: '/users/:id/',
+ transaction_info: {
+ source: 'route',
+ },
+ });
+
+ const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing');
+
+ expect(routingSpan).toBeDefined();
+ expect(routingSpan?.description).toBe('/redirect1');
+});
+
+test.describe('finish routing span', () => {
+ test('finishes routing span on navigation cancel', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#cancelLink').click(), navigationTxnPromise]);
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.angular',
+ },
+ },
+ transaction: '/cancel',
+ transaction_info: {
+ source: 'url',
+ },
+ });
+
+ const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing');
+
+ expect(routingSpan).toBeDefined();
+ expect(routingSpan?.description).toBe('/cancel');
+ });
+
+ test('finishes routing span on navigation error', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#nonExistentLink').click(), navigationTxnPromise]);
+
+ const nonExistentRoute = '/non-existent';
+
+ expect(navigationTxn).toMatchObject({
+ contexts: {
+ trace: {
+ op: 'navigation',
+ origin: 'auto.navigation.angular',
+ },
+ },
+ transaction: nonExistentRoute,
+ transaction_info: {
+ source: 'url',
+ },
+ });
+
+ const routingSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.routing');
+
+ expect(routingSpan).toBeDefined();
+ expect(routingSpan?.description).toBe(nonExistentRoute);
+ });
+});
+
+test.describe('TraceDirective', () => {
+ test('creates a child tracingSpan with component name as span name on ngOnInit', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
+
+ const traceDirectiveSpan = navigationTxn.spans?.find(
+ span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_directive',
+ );
+
+ expect(traceDirectiveSpan).toBeDefined();
+ expect(traceDirectiveSpan).toEqual(
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_directive',
+ },
+ description: '',
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_directive',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ );
+ });
+});
+
+test.describe('TraceClass Decorator', () => {
+ test('adds init span for decorated class', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
+
+ const classDecoratorSpan = navigationTxn.spans?.find(
+ span => span?.data && span?.data[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] === 'auto.ui.angular.trace_class_decorator',
+ );
+
+ expect(classDecoratorSpan).toBeDefined();
+ expect(classDecoratorSpan).toEqual(
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.init',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_class_decorator',
+ },
+ description: '',
+ op: 'ui.angular.init',
+ origin: 'auto.ui.angular.trace_class_decorator',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ );
+ });
+});
+
+test.describe('TraceMethod Decorator', () => {
+ test('adds name to span description of decorated method `ngOnInit`', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
+
+ const ngInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngOnInit');
+
+ expect(ngInitSpan).toBeDefined();
+ expect(ngInitSpan).toEqual(
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngOnInit',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator',
+ },
+ description: '',
+ op: 'ui.angular.ngOnInit',
+ origin: 'auto.ui.angular.trace_method_decorator',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ );
+ });
+
+ test('adds fallback name to span description of decorated method `ngAfterViewInit`', async ({ page }) => {
+ const navigationTxnPromise = waitForTransaction('angular-18', async transactionEvent => {
+ return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
+ });
+
+ await page.goto(`/`);
+
+ // immediately navigate to a different route
+ const [_, navigationTxn] = await Promise.all([page.locator('#componentTracking').click(), navigationTxnPromise]);
+
+ const ngAfterViewInitSpan = navigationTxn.spans?.find(span => span.op === 'ui.angular.ngAfterViewInit');
+
+ expect(ngAfterViewInitSpan).toBeDefined();
+ expect(ngAfterViewInitSpan).toEqual(
+ expect.objectContaining({
+ data: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.angular.ngAfterViewInit',
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.angular.trace_method_decorator',
+ },
+ description: '',
+ op: 'ui.angular.ngAfterViewInit',
+ origin: 'auto.ui.angular.trace_method_decorator',
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ }),
+ );
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json
new file mode 100644
index 000000000000..3775b37e3bbc
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.app.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": [
+ "src/main.ts"
+ ],
+ "include": [
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.json b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.json
new file mode 100644
index 000000000000..5525117c6744
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.json
@@ -0,0 +1,27 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "outDir": "./dist/out-tsc",
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "bundler",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json
new file mode 100644
index 000000000000..5fb748d9207a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/angular-19/tsconfig.spec.json
@@ -0,0 +1,15 @@
+/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
+/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": [
+ "jasmine"
+ ]
+ },
+ "include": [
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/packages/angular/package.json b/packages/angular/package.json
index b62299574a84..af01c95686ed 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -15,9 +15,9 @@
"access": "public"
},
"peerDependencies": {
- "@angular/common": ">= 14.x <= 18.x",
- "@angular/core": ">= 14.x <= 18.x",
- "@angular/router": ">= 14.x <= 18.x",
+ "@angular/common": ">= 14.x <= 19.x",
+ "@angular/core": ">= 14.x <= 19.x",
+ "@angular/router": ">= 14.x <= 19.x",
"rxjs": "^6.5.5 || ^7.x"
},
"dependencies": {
@@ -65,10 +65,7 @@
"nx": {
"targets": {
"build:transpile": {
- "dependsOn": [
- "^build:transpile",
- "^build:types"
- ],
+ "dependsOn": ["^build:transpile", "^build:types"],
"outputs": [
"{projectRoot}/build/esm2015",
"{projectRoot}/build/fesm2015",