diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/.gitignore b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/.gitignore
new file mode 100644
index 000000000000..a547bf36d8d1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/docker-compose.yml b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/docker-compose.yml
new file mode 100644
index 000000000000..954690210f58
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/docker-compose.yml
@@ -0,0 +1,31 @@
+services:
+ db:
+ image: mysql:8.0
+ restart: always
+ container_name: e2e-tests-tanstackstart-react-static-mysql
+ # The `mysql` 2.x driver doesn't speak MySQL 8's default
+ # `caching_sha2_password` auth, so force the legacy plugin.
+ command: ['--default-authentication-plugin=mysql_native_password']
+ ports:
+ - '3306:3306'
+ environment:
+ MYSQL_ROOT_PASSWORD: docker
+ healthcheck:
+ test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pdocker']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 10s
+
+ redis:
+ image: redis:7
+ restart: always
+ container_name: e2e-tests-tanstackstart-react-static-redis
+ ports:
+ - '6379:6379'
+ healthcheck:
+ test: ['CMD', 'redis-cli', 'ping']
+ interval: 2s
+ timeout: 3s
+ retries: 30
+ start_period: 5s
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-setup.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-setup.mjs
new file mode 100644
index 000000000000..8f5f52009158
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-setup.mjs
@@ -0,0 +1,14 @@
+import { execSync } from 'child_process';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default async function globalSetup() {
+ // Start MySQL and Redis via Docker Compose. `--wait` blocks until the
+ // healthchecks in docker-compose.yml pass, so the app can connect immediately.
+ execSync('docker compose up -d --wait', {
+ cwd: __dirname,
+ stdio: 'inherit',
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-teardown.mjs
new file mode 100644
index 000000000000..2742279431ad
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-teardown.mjs
@@ -0,0 +1,12 @@
+import { execSync } from 'child_process';
+import { dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+export default async function globalTeardown() {
+ execSync('docker compose down --volumes', {
+ cwd: __dirname,
+ stdio: 'inherit',
+ });
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/instrument.server.mjs
new file mode 100644
index 000000000000..d9bccefea72a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/instrument.server.mjs
@@ -0,0 +1,13 @@
+import * as Sentry from '@sentry/tanstackstart-react';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+ traceLifecycle: 'static',
+ transportOptions: {
+ // We expect the app to send a lot of events in a short time
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json
new file mode 100644
index 000000000000..ff237684be5d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "tanstackstart-react-static",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "scripts": {
+ "build": "vite build && cp instrument.server.mjs .output/server",
+ "dev": "vite dev",
+ "start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs",
+ "test": "playwright test",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test:build": "pnpm install && pnpm build",
+ "test:build:tunnel-generated": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
+ "test:build:tunnel-static": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
+ "test:build:tunnel-custom": "pnpm install && E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm build",
+ "test:build:tunnel-object": "pnpm install && E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm build",
+ "test:build-latest": "pnpm add @tanstack/react-start@latest @tanstack/react-router@latest && pnpm install && pnpm build",
+ "test:assert:proxy": "pnpm test && TEST_ENV=development pnpm playwright test db-drivers",
+ "test:assert": "pnpm test:assert:proxy",
+ "test:assert:tunnel-generated": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
+ "test:assert:tunnel-static": "E2E_TEST_TUNNEL_ROUTE_MODE=static E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test",
+ "test:assert:tunnel-custom": "E2E_TEST_CUSTOM_TUNNEL_ROUTE=1 pnpm test",
+ "test:assert:tunnel-object": "E2E_TEST_TUNNEL_ROUTE_MODE=object E2E_TEST_DSN=http://public@localhost:3031/1337 pnpm test"
+ },
+ "//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels",
+ "dependencies": {
+ "@sentry/tanstackstart-react": "file:../../packed/sentry-tanstackstart-react-packed.tgz",
+ "@tanstack/react-start": "1.168.35",
+ "@tanstack/react-router": "1.170.18",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "nitro": "latest || *",
+ "ioredis": "5.10.1",
+ "mysql": "^2.18.1"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.0",
+ "@types/react-dom": "^19.2.0",
+ "@types/node": "^24.10.0",
+ "@typescript-eslint/eslint-plugin": "^7.2.0",
+ "@typescript-eslint/parser": "^7.2.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "typescript": "^5.9.0",
+ "vite": "7.3.2",
+ "vite-tsconfig-paths": "^5.1.4",
+ "@playwright/test": "~1.56.0",
+ "@sentry-internal/test-utils": "link:../../../test-utils"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/playwright.config.mjs
new file mode 100644
index 000000000000..cc18dd0ea437
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/playwright.config.mjs
@@ -0,0 +1,26 @@
+import { getPlaywrightConfig } from '@sentry-internal/test-utils';
+
+// The DB driver tests only run in the default (proxy) variant, so the tunnel-route variants don't
+// need MySQL/Redis — skip Docker there instead of booting the containers once per variant.
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+// The dev server has no build output, so `--import` points at the app's own instrumentation file.
+// `vite dev` ignores PORT, so the port goes on the command.
+const startCommand =
+ process.env.TEST_ENV === 'development'
+ ? `NODE_OPTIONS='--import ./instrument.server.mjs' pnpm dev --port 3000`
+ : `pnpm start`;
+
+const config = getPlaywrightConfig({
+ startCommand,
+ port: 3000,
+});
+
+export default usesManagedTunnelRoute
+ ? config
+ : {
+ ...config,
+ globalSetup: './global-setup.mjs',
+ globalTeardown: './global-teardown.mjs',
+ };
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx
new file mode 100644
index 000000000000..fc489cd7bdeb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx
@@ -0,0 +1,21 @@
+// Imported first so Sentry.init() runs before any other import's side effects.
+import './instrument.client';
+import './early-side-effect';
+import { StartClient } from '@tanstack/react-start/client';
+import { StrictMode, startTransition } from 'react';
+import { hydrateRoot } from 'react-dom/client';
+
+console.log('early-breadcrumb-from-client-entry');
+
+if (window.location.pathname === '/crash-before-hydration') {
+ throw new Error('Client Entry Crash');
+}
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+ ,
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts
new file mode 100644
index 000000000000..1118a2e72f1a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts
@@ -0,0 +1,7 @@
+// Simulates a third-party import whose side effects run at module-evaluation time,
+// i.e. before the client entry's body (and before hydration).
+console.log('early-breadcrumb-from-imported-module');
+
+if (typeof window !== 'undefined' && window.location.pathname === '/crash-in-imported-module') {
+ throw new Error('Imported Module Side Effect Crash');
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts
new file mode 100644
index 000000000000..6e7d31c7a4e6
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts
@@ -0,0 +1,2 @@
+declare const __APP_DSN__: string;
+declare const __APP_TUNNEL__: string | undefined;
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts
new file mode 100644
index 000000000000..95351c60f9ca
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts
@@ -0,0 +1,10 @@
+import * as Sentry from '@sentry/tanstackstart-react';
+
+Sentry.init({
+ traceLifecycle: 'static',
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: __APP_DSN__,
+ tracesSampleRate: 1.0,
+ release: 'e2e-test',
+ tunnel: __APP_TUNNEL__,
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts
new file mode 100644
index 000000000000..a15bd3744910
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts
@@ -0,0 +1,41 @@
+import { createMiddleware } from '@tanstack/react-start';
+
+// Global request middleware - runs on every request
+// NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin
+export const globalRequestMiddleware = createMiddleware().server(async ({ next }) => {
+ console.log('Global request middleware executed');
+ return next();
+});
+
+// Global function middleware - runs on every server function
+// NOTE: This is exported unwrapped to test auto-instrumentation via the Vite plugin
+export const globalFunctionMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ console.log('Global function middleware executed');
+ return next();
+});
+
+// Server function middleware - exported unwrapped for auto-instrumentation via Vite plugin
+export const serverFnMiddleware = createMiddleware({ type: 'function' }).server(async ({ next }) => {
+ console.log('Server function middleware executed');
+ return next();
+});
+
+// Server route request middleware - exported unwrapped for auto-instrumentation via Vite plugin
+export const serverRouteRequestMiddleware = createMiddleware().server(async ({ next }) => {
+ console.log('Server route request middleware executed');
+ return next();
+});
+
+// Early return middleware - returns without calling next()
+// Exported unwrapped for auto-instrumentation via Vite plugin
+export const earlyReturnMiddleware = createMiddleware({ type: 'function' }).server(async () => {
+ console.log('Early return middleware executed - not calling next()');
+ return { earlyReturn: true, message: 'Middleware returned early without calling next()' };
+});
+
+// Error middleware - throws an exception
+// Exported unwrapped for auto-instrumentation via Vite plugin
+export const errorMiddleware = createMiddleware({ type: 'function' }).server(async () => {
+ console.log('Error middleware executed - throwing error');
+ throw new Error('Middleware Error Test');
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx
new file mode 100644
index 000000000000..670b22bc688d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx
@@ -0,0 +1,16 @@
+import * as Sentry from '@sentry/tanstackstart-react';
+import { createRouter } from '@tanstack/react-router';
+import { routeTree } from './routeTree.gen';
+
+export const getRouter = () => {
+ const router = createRouter({
+ routeTree,
+ scrollRestoration: true,
+ });
+
+ if (!router.isServer) {
+ Sentry.addIntegration(Sentry.tanstackRouterBrowserTracingIntegration(router));
+ }
+
+ return router;
+};
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx
new file mode 100644
index 000000000000..204276e1bdf2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx
@@ -0,0 +1,50 @@
+import { useEffect, type ReactNode } from 'react';
+import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
+
+export const Route = createRootRoute({
+ head: () => ({
+ meta: [
+ {
+ charSet: 'utf-8',
+ },
+ {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1',
+ },
+ {
+ title: 'TanStack Start Starter',
+ },
+ ],
+ }),
+ component: RootComponent,
+});
+
+function RootComponent() {
+ // Mark the document as hydrated so tests can wait for React to attach event
+ // handlers before clicking. `toBeVisible()` only confirms the SSR HTML is
+ // rendered; on slow CI runs Playwright can click before hydration completes,
+ // the onClick handler isn't yet attached, and tests asserting on the
+ // resulting client-side error time out (see #20641, #20685, #20867).
+ useEffect(() => {
+ document.documentElement.setAttribute('data-hydrated', 'true');
+ }, []);
+ return (
+
+
+
+ );
+}
+
+function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts
new file mode 100644
index 000000000000..c3453f5be70f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts
@@ -0,0 +1,24 @@
+import { createFileRoute } from '@tanstack/react-router';
+import Redis from 'ioredis';
+
+export const Route = createFileRoute('/api/db-ioredis')({
+ server: {
+ handlers: {
+ GET: async () => {
+ const redis = new Redis({
+ // Don't keep retrying forever if Redis goes away (e.g. on test teardown)
+ maxRetriesPerRequest: 1,
+ retryStrategy: () => null,
+ });
+
+ try {
+ await redis.set('test-key', 'test-value');
+ const value = await redis.get('test-key');
+ return Response.json({ value });
+ } finally {
+ redis.disconnect();
+ }
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts
new file mode 100644
index 000000000000..3c5e634c015b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts
@@ -0,0 +1,23 @@
+import { createFileRoute } from '@tanstack/react-router';
+import mysql from 'mysql';
+
+const connection = mysql.createConnection({
+ user: 'root',
+ password: 'docker',
+});
+
+export const Route = createFileRoute('/api/db-mysql')({
+ server: {
+ handlers: {
+ GET: () => {
+ return new Promise(resolve => {
+ connection.query('SELECT 1 + 1 AS solution', () => {
+ connection.query('SELECT NOW()', ['1', '2'], () => {
+ resolve(Response.json({ status: 'ok' }));
+ });
+ });
+ });
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts
new file mode 100644
index 000000000000..041fb175c1f1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts
@@ -0,0 +1,11 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/api/error')({
+ server: {
+ handlers: {
+ GET: async () => {
+ throw new Error('Sentry API Route Test Error');
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts
new file mode 100644
index 000000000000..fb296b07d9b0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { flush } from '@sentry/tanstackstart-react';
+
+export const Route = createFileRoute('/api/flush')({
+ server: {
+ handlers: {
+ GET: async () => {
+ await flush();
+ return new Response('ok');
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts
new file mode 100644
index 000000000000..d24f8ed61a45
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { serverRouteRequestMiddleware } from '../middleware';
+
+export const Route = createFileRoute('/api/test-middleware')({
+ server: {
+ middleware: [serverRouteRequestMiddleware],
+ handlers: {
+ GET: async () => {
+ return { message: 'Server route middleware test' };
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts
new file mode 100644
index 000000000000..ce20e2f74ce1
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/api/user/$id')({
+ server: {
+ handlers: {
+ GET: async ({ params }) => {
+ return new Response(JSON.stringify({ id: params.id }), {
+ headers: { 'Content-Type': 'application/json' },
+ });
+ },
+ },
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx
new file mode 100644
index 000000000000..772a045a7e33
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx
@@ -0,0 +1,13 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/crash-before-hydration')({
+ component: CrashPage,
+});
+
+function CrashPage() {
+ return (
+
+
This page crashes in client.tsx before hydration
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts
new file mode 100644
index 000000000000..1409123f0402
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/tanstackstart-react';
+import { createFileRoute } from '@tanstack/react-router';
+
+const USE_CUSTOM_TUNNEL_ROUTE = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+const DEFAULT_DSN = 'https://public@dsn.ingest.sentry.io/1337';
+const TUNNEL_DSN = 'http://public@localhost:3031/1337';
+
+// Example of a manually defined tunnel endpoint without relying on the
+// managed route injected by `sentryTanstackStart({ tunnelRoute: ... })`.
+// If you use a custom route like this one, set `tunnel: '/custom-monitor'` in the client SDK's
+// `Sentry.init()` call so browser events are sent to the same endpoint.
+export const Route = createFileRoute('/custom-monitor')({
+ server: Sentry.createSentryTunnelRoute({
+ allowedDsns: [USE_CUSTOM_TUNNEL_ROUTE ? TUNNEL_DSN : DEFAULT_DSN],
+ }),
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx
new file mode 100644
index 000000000000..a92fabf20c4a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx
@@ -0,0 +1,41 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { createServerFn } from '@tanstack/react-start';
+
+const throwServerError = createServerFn().handler(async () => {
+ throw new Error('Sentry Server Function Test Error');
+});
+
+export const Route = createFileRoute('/')({
+ component: Home,
+});
+
+function Home() {
+ return (
+
+ {
+ throw new Error('Sentry Client Test Error');
+ }}
+ >
+ Break the client
+
+ {
+ await throwServerError();
+ }}
+ >
+ Break server function
+
+ {
+ await fetch('/api/error');
+ }}
+ >
+ Break API route
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx
new file mode 100644
index 000000000000..43d32823168e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/param/$id')({
+ component: ParamPage,
+});
+
+function ParamPage() {
+ const { id } = Route.useParams();
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx
new file mode 100644
index 000000000000..71ba7ce92d29
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx
@@ -0,0 +1,8 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/ssr-error')({
+ loader: () => {
+ throw new Error('Sentry SSR Test Error');
+ },
+ component: () => SSR Error Page
,
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx
new file mode 100644
index 000000000000..81d621b92a46
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx
@@ -0,0 +1,86 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { createServerFn } from '@tanstack/react-start';
+import { serverFnMiddleware, earlyReturnMiddleware, errorMiddleware } from '../middleware';
+
+// Server function with specific middleware (also gets global function middleware)
+const serverFnWithMiddleware = createServerFn()
+ .middleware([serverFnMiddleware])
+ .handler(async () => {
+ console.log('Server function with specific middleware executed');
+ return { message: 'Server function middleware test' };
+ });
+
+// Server function without specific middleware (only gets global function middleware)
+const serverFnWithoutMiddleware = createServerFn().handler(async () => {
+ console.log('Server function without specific middleware executed');
+ return { message: 'Global middleware only test' };
+});
+
+// Server function with early return middleware (middleware returns without calling next)
+const serverFnWithEarlyReturnMiddleware = createServerFn()
+ .middleware([earlyReturnMiddleware])
+ .handler(async () => {
+ console.log('This should not be executed - middleware returned early');
+ return { message: 'This should not be returned' };
+ });
+
+// Server function with error middleware (middleware throws an error)
+const serverFnWithErrorMiddleware = createServerFn()
+ .middleware([errorMiddleware])
+ .handler(async () => {
+ console.log('This should not be executed - middleware threw error');
+ return { message: 'This should not be returned' };
+ });
+
+export const Route = createFileRoute('/test-middleware')({
+ component: TestMiddleware,
+});
+
+function TestMiddleware() {
+ return (
+
+
Test Middleware Page
+ {
+ await serverFnWithMiddleware();
+ }}
+ >
+ Call server function with middleware
+
+ {
+ await serverFnWithoutMiddleware();
+ }}
+ >
+ Call server function (global middleware only)
+
+ {
+ const result = await serverFnWithEarlyReturnMiddleware();
+ console.log('Early return result:', result);
+ }}
+ >
+ Call server function with early return middleware
+
+ {
+ try {
+ await serverFnWithErrorMiddleware();
+ } catch (error) {
+ console.log('Caught error from middleware:', error);
+ }
+ }}
+ >
+ Call server function with error middleware
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx
new file mode 100644
index 000000000000..894ff4e85ee3
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx
@@ -0,0 +1,45 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { createServerFn } from '@tanstack/react-start';
+import { startSpan } from '@sentry/tanstackstart-react';
+
+const testLog = createServerFn().handler(async () => {
+ console.log('Test log from server function');
+ return { message: 'Log created' };
+});
+
+const testNestedLog = createServerFn().handler(async () => {
+ await startSpan({ name: 'testNestedLog' }, async () => {
+ await testLog();
+ });
+
+ console.log('Outer test log from server function');
+ return { message: 'Nested log created' };
+});
+
+export const Route = createFileRoute('/test-serverFn')({
+ component: TestLog,
+});
+
+function TestLog() {
+ return (
+
+
Test Log Page
+ {
+ await testLog();
+ }}
+ >
+ Call server function
+
+ {
+ await testNestedLog();
+ }}
+ >
+ Call server function nested
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx
new file mode 100644
index 000000000000..60f16f5cfece
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx
@@ -0,0 +1,14 @@
+import { createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/users/$userId')({
+ component: UserPage,
+});
+
+function UserPage() {
+ const { userId } = Route.useParams();
+ return (
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx
new file mode 100644
index 000000000000..97684c05df19
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx
@@ -0,0 +1,13 @@
+import { Outlet, createFileRoute } from '@tanstack/react-router';
+
+export const Route = createFileRoute('/users')({
+ component: UsersLayout,
+});
+
+function UsersLayout() {
+ return (
+
+
+
+ );
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts
new file mode 100644
index 000000000000..b10a3bc1e37b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts
@@ -0,0 +1,12 @@
+import { wrapFetchWithSentry } from '@sentry/tanstackstart-react';
+
+import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
+import type { ServerEntry } from '@tanstack/react-start/server-entry';
+
+const requestHandler: ServerEntry = wrapFetchWithSentry({
+ fetch(request: Request) {
+ return handler.fetch(request);
+ },
+});
+
+export default createServerEntry(requestHandler);
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts
new file mode 100644
index 000000000000..110e3602b1c9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts
@@ -0,0 +1,11 @@
+import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware } from '@sentry/tanstackstart-react';
+import { createStart } from '@tanstack/react-start';
+// NOTE: These are NOT wrapped - auto-instrumentation via the Vite plugin will wrap them
+import { globalFunctionMiddleware, globalRequestMiddleware } from './middleware';
+
+export const startInstance = createStart(() => {
+ return {
+ requestMiddleware: [sentryGlobalRequestMiddleware, globalRequestMiddleware],
+ functionMiddleware: [sentryGlobalFunctionMiddleware, globalFunctionMiddleware],
+ };
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/start-event-proxy.mjs
new file mode 100644
index 000000000000..a3bc0afc007f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'tanstackstart-react-static',
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/db-drivers.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/db-drivers.test.ts
new file mode 100644
index 000000000000..32858b1c2eb0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/db-drivers.test.ts
@@ -0,0 +1,98 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+// Same spans in both runs, from two injectors: the orchestrion build-time transform that
+// `sentryTanstackStart()` auto-wires into the server bundle, and the runtime hook in `vite dev`,
+// where the drivers stay external on Node's own loader.
+test('Instruments ioredis automatically', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/db-ioredis'
+ );
+ });
+
+ await fetch(`${baseURL}/api/db-ioredis`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ op: 'db.query',
+ origin: 'auto.db.redis',
+ description: 'set test-key [1 other arguments]',
+ status: 'ok',
+ data: expect.objectContaining({
+ 'db.system.name': 'redis',
+ 'db.operation.name': 'set',
+ 'db.query.text': 'set test-key [1 other arguments]',
+ }),
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ op: 'db.query',
+ origin: 'auto.db.redis',
+ description: 'get test-key',
+ status: 'ok',
+ data: expect.objectContaining({
+ 'db.system.name': 'redis',
+ 'db.operation.name': 'get',
+ 'db.query.text': 'get test-key',
+ }),
+ }),
+ );
+});
+
+test('Instruments mysql automatically', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /api/db-mysql'
+ );
+ });
+
+ await fetch(`${baseURL}/api/db-mysql`);
+
+ const transactionEvent = await transactionEventPromise;
+
+ const spans = transactionEvent.spans || [];
+
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ op: 'db',
+ origin: 'auto.db.mysql',
+ description: 'SELECT 1 + 1 AS solution',
+ status: 'ok',
+ data: expect.objectContaining({
+ 'db.system.name': 'mysql',
+ 'db.query.text': 'SELECT 1 + 1 AS solution',
+ 'db.user': 'root',
+ 'db.connection_string': expect.any(String),
+ 'server.address': expect.any(String),
+ 'server.port': 3306,
+ }),
+ }),
+ );
+ expect(spans).toContainEqual(
+ expect.objectContaining({
+ op: 'db',
+ origin: 'auto.db.mysql',
+ description: 'SELECT NOW()',
+ status: 'ok',
+ data: expect.objectContaining({
+ 'db.system.name': 'mysql',
+ 'db.query.text': 'SELECT NOW()',
+ 'db.user': 'root',
+ 'db.connection_string': expect.any(String),
+ 'server.address': expect.any(String),
+ 'server.port': 3306,
+ }),
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/early-init.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/early-init.test.ts
new file mode 100644
index 000000000000..914537d8554e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/early-init.test.ts
@@ -0,0 +1,49 @@
+import { expect, test } from '@playwright/test';
+import { waitForError } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test('should capture errors thrown in client entry before hydration', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Client Entry Crash';
+ });
+
+ await page.goto('/crash-before-hydration');
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values?.[0]).toMatchObject({
+ type: 'Error',
+ value: 'Client Entry Crash',
+ });
+
+ const consoleBreadcrumbs = errorEvent.breadcrumbs?.filter(
+ b => b.category === 'console' && b.message?.includes('early-breadcrumb-from-client-entry'),
+ );
+
+ expect(consoleBreadcrumbs?.length).toBeGreaterThanOrEqual(1);
+});
+
+test('should capture errors thrown in a module imported by the client entry before hydration', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Imported Module Side Effect Crash';
+ });
+
+ await page.goto('/crash-in-imported-module');
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values?.[0]).toMatchObject({
+ type: 'Error',
+ value: 'Imported Module Side Effect Crash',
+ });
+
+ const consoleBreadcrumbs = errorEvent.breadcrumbs?.filter(
+ b => b.category === 'console' && b.message?.includes('early-breadcrumb-from-imported-module'),
+ );
+
+ expect(consoleBreadcrumbs?.length).toBeGreaterThanOrEqual(1);
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/errors.test.ts
new file mode 100644
index 000000000000..765bc5d7caa0
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/errors.test.ts
@@ -0,0 +1,140 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
+ });
+
+ await page.goto(`/`);
+
+ // Wait for React to hydrate (see __root.tsx) before clicking — the SSR HTML
+ // renders the button before the onClick handler is attached, and clicking
+ // pre-hydration would fire no handler and produce no error.
+ await page.locator('html[data-hydrated="true"]').waitFor();
+
+ await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible();
+
+ await page.locator('button').filter({ hasText: 'Break the client' }).click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry Client Test Error',
+ mechanism: {
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+
+ expect(errorEvent.transaction).toBe('/');
+});
+
+test('Sends server-side function error to Sentry with auto-instrumentation', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ // The thrown error propagates back to the client over the server-function RPC and is also
+ // captured there as an `onunhandledrejection` with the same message. Match on the server-function
+ // mechanism so we deterministically pick the server-side event instead of racing the client one.
+ return (
+ errorEvent?.exception?.values?.[0]?.value === 'Sentry Server Function Test Error' &&
+ errorEvent?.exception?.values?.[0]?.mechanism?.type === 'auto.middleware.tanstackstart.server_function'
+ );
+ });
+
+ await page.goto(`/`);
+
+ await expect(page.locator('button').filter({ hasText: 'Break server function' })).toBeVisible();
+
+ await page.locator('button').filter({ hasText: 'Break server function' }).click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry Server Function Test Error',
+ mechanism: {
+ type: 'auto.middleware.tanstackstart.server_function',
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+
+ expect(errorEvent.transaction).toEqual(expect.stringContaining('GET /_serverFn/'));
+});
+
+test('Sends API route error to Sentry with auto-instrumentation', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ // As with the server-function test, guard against a same-message client-side duplicate by
+ // matching the server request mechanism, so we always assert against the server-side event.
+ return (
+ errorEvent?.exception?.values?.[0]?.value === 'Sentry API Route Test Error' &&
+ errorEvent?.exception?.values?.[0]?.mechanism?.type === 'auto.middleware.tanstackstart.request'
+ );
+ });
+
+ await page.goto(`/`);
+
+ await expect(page.locator('button').filter({ hasText: 'Break API route' })).toBeVisible();
+
+ await page.locator('button').filter({ hasText: 'Break API route' }).click();
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent).toMatchObject({
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry API Route Test Error',
+ mechanism: {
+ type: 'auto.middleware.tanstackstart.request',
+ handled: false,
+ },
+ },
+ ],
+ },
+ });
+
+ expect(errorEvent.transaction).toBe('GET /api/error');
+});
+
+// the sentry global middleware does not capture errors from SSR loader errors since they are serialized before they reach the middleware layer
+// this test verifies that the error is in fact not sent to Sentry
+test('Does not send SSR loader error to Sentry', async ({ baseURL, page }) => {
+ let errorEventOccurred = false;
+
+ waitForError('tanstackstart-react-static', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'Sentry SSR Test Error') {
+ errorEventOccurred = true;
+ }
+ return event?.transaction === 'GET /ssr-error';
+ });
+
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /ssr-error';
+ });
+
+ await page.goto('/ssr-error');
+
+ await transactionEventPromise;
+
+ await (await fetch(`${baseURL}/api/flush`)).text();
+
+ expect(errorEventOccurred).toBe(false);
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/middleware.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/middleware.test.ts
new file mode 100644
index 000000000000..7a55b56d282c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/middleware.test.ts
@@ -0,0 +1,197 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test('Sends spans for multiple middlewares and verifies they are siblings under the same parent span', async ({
+ page,
+}) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-middleware-btn')).toBeVisible();
+ await page.locator('#server-fn-middleware-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Find both middleware spans
+ const serverFnMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'serverFnMiddleware' && span.origin === 'auto.middleware.tanstackstart',
+ );
+ const globalFunctionMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'globalFunctionMiddleware' && span.origin === 'auto.middleware.tanstackstart',
+ );
+
+ // Verify both middleware spans exist with expected properties
+ expect(serverFnMiddlewareSpan).toEqual(
+ expect.objectContaining({
+ description: 'serverFnMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ );
+ expect(globalFunctionMiddlewareSpan).toEqual(
+ expect.objectContaining({
+ description: 'globalFunctionMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ );
+
+ // Both middleware spans should be siblings under the same parent
+ expect(serverFnMiddlewareSpan?.parent_span_id).toBe(globalFunctionMiddlewareSpan?.parent_span_id);
+});
+
+test('Sends spans for global function middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-global-only-btn')).toBeVisible();
+ await page.locator('#server-fn-global-only-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the global function middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'globalFunctionMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends spans for global request middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-middleware'
+ );
+ });
+
+ await page.goto('/test-middleware');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the global request middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'globalRequestMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends spans for server route request middleware', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /api/test-middleware'
+ );
+ });
+
+ await page.goto('/api/test-middleware');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the server route request middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'serverRouteRequestMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends span for middleware that returns early without calling next()', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-early-return-btn')).toBeVisible();
+ await page.locator('#server-fn-early-return-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the early return middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'earlyReturnMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends span for middleware that throws an error', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-middleware');
+ await expect(page.locator('#server-fn-error-btn')).toBeVisible();
+ await page.locator('#server-fn-error-btn').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the error middleware span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'errorMiddleware',
+ op: 'middleware',
+ origin: 'auto.middleware.tanstackstart',
+ }),
+ ]),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/route-parametrization.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/route-parametrization.test.ts
new file mode 100644
index 000000000000..3e94c7ce7bb9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/route-parametrization.test.ts
@@ -0,0 +1,82 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test('should parametrize server and client transaction names for dynamic routes', async ({ page }) => {
+ const serverTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ typeof transactionEvent?.transaction === 'string' &&
+ transactionEvent.transaction.includes('/param/')
+ );
+ });
+
+ const clientTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'pageload' &&
+ typeof transactionEvent?.transaction === 'string' &&
+ transactionEvent.transaction.includes('/param/')
+ );
+ });
+
+ await page.goto('/param/42');
+
+ const serverTx = await serverTxPromise;
+ const clientTx = await clientTxPromise;
+
+ expect(serverTx.transaction).toBe('GET /param/$id');
+ expect(serverTx.transaction_info?.source).toBe('route');
+
+ expect(clientTx.transaction).toBe('/param/$id');
+ expect(clientTx.transaction_info?.source).toBe('route');
+});
+
+test('should parametrize server and client transaction names for nested dynamic routes', async ({ page }) => {
+ const serverTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ typeof transactionEvent?.transaction === 'string' &&
+ transactionEvent.transaction.includes('/users/')
+ );
+ });
+
+ const clientTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'pageload' &&
+ typeof transactionEvent?.transaction === 'string' &&
+ transactionEvent.transaction.includes('/users/')
+ );
+ });
+
+ await page.goto('/users/123');
+
+ const serverTx = await serverTxPromise;
+ const clientTx = await clientTxPromise;
+
+ expect(serverTx.transaction).toBe('GET /users/$userId');
+ expect(serverTx.transaction_info?.source).toBe('route');
+
+ expect(clientTx.transaction).toBe('/users/$userId');
+ expect(clientTx.transaction_info?.source).toBe('route');
+});
+
+test('should parametrize API route transaction names', async ({ baseURL }) => {
+ const serverTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ typeof transactionEvent?.transaction === 'string' &&
+ transactionEvent.transaction.includes('/api/user/')
+ );
+ });
+
+ await fetch(`${baseURL}/api/user/456`);
+
+ const serverTx = await serverTxPromise;
+
+ expect(serverTx.transaction).toBe('GET /api/user/$id');
+ expect(serverTx.transaction_info?.source).toBe('route');
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/trace-propagation.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/trace-propagation.test.ts
new file mode 100644
index 000000000000..8acaa89f791d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/trace-propagation.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test.describe('Trace propagation', () => {
+ test('should inject metatags in ssr pageload', async ({ page }) => {
+ await page.goto('/');
+
+ const sentryTraceContent = await page.getAttribute('meta[name="sentry-trace"]', 'content');
+ expect(sentryTraceContent).toBeDefined();
+ expect(sentryTraceContent).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}-[01]$/);
+
+ const baggageContent = await page.getAttribute('meta[name="baggage"]', 'content');
+ expect(baggageContent).toBeDefined();
+ expect(baggageContent).toContain('sentry-environment=qa');
+ expect(baggageContent).toContain('sentry-public_key=');
+ expect(baggageContent).toContain('sentry-trace_id=');
+ expect(baggageContent).toContain('sentry-sampled=');
+ });
+
+ test('should have trace connection between server and client', async ({ page }) => {
+ const serverTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /';
+ });
+
+ const clientTxPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/';
+ });
+
+ await page.goto('/');
+
+ const serverTx = await serverTxPromise;
+ const clientTx = await clientTxPromise;
+
+ expect(clientTx.contexts?.trace?.trace_id).toBe(serverTx.contexts?.trace?.trace_id);
+ });
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts
new file mode 100644
index 000000000000..ad8cd5e9dfa9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts
@@ -0,0 +1,109 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+const usesManagedTunnelRoute =
+ (process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off') !== 'off' || process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+test.skip(usesManagedTunnelRoute, 'Default e2e suites run only in the proxy variant');
+
+test('Sends a server function transaction with auto-instrumentation', async ({ page }) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-serverFn');
+
+ await expect(page.getByText('Call server function', { exact: true })).toBeVisible();
+
+ await page.getByText('Call server function', { exact: true }).click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ // Check for the auto-instrumented server function span
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'GET /_serverFn/testLog',
+ op: 'function',
+ origin: 'auto.function.tanstackstart.server',
+ data: {
+ 'sentry.op': 'function',
+ 'sentry.origin': 'auto.function.tanstackstart.server',
+ 'tanstackstart.function.id': expect.any(String),
+ 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx',
+ },
+ status: 'ok',
+ }),
+ ]),
+ );
+});
+
+test('Sends a server function transaction for a nested server function only if it is manually instrumented', async ({
+ page,
+}) => {
+ const transactionEventPromise = waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ !!transactionEvent?.transaction?.startsWith('GET /_serverFn')
+ );
+ });
+
+ await page.goto('/test-serverFn');
+
+ await expect(page.getByText('Call server function nested')).toBeVisible();
+
+ await page.getByText('Call server function nested').click();
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(Array.isArray(transactionEvent?.spans)).toBe(true);
+
+ // Check for the auto-instrumented server function span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'GET /_serverFn/testNestedLog',
+ op: 'function',
+ origin: 'auto.function.tanstackstart.server',
+ data: {
+ 'sentry.op': 'function',
+ 'sentry.origin': 'auto.function.tanstackstart.server',
+ 'tanstackstart.function.id': expect.any(String),
+ 'tanstackstart.function.filename': 'src/routes/test-serverFn.tsx',
+ },
+ status: 'ok',
+ }),
+ ]),
+ );
+
+ // Check for the manually instrumented nested span
+ expect(transactionEvent?.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ description: 'testNestedLog',
+ origin: 'manual',
+ status: 'ok',
+ }),
+ ]),
+ );
+
+ // Verify that globalFunctionMiddleware and testNestedLog are sibling spans under the root
+ const functionMiddlewareSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'globalFunctionMiddleware' && span.origin === 'auto.middleware.tanstackstart',
+ );
+ const nestedSpan = transactionEvent?.spans?.find(
+ (span: { description?: string; origin?: string }) =>
+ span.description === 'testNestedLog' && span.origin === 'manual',
+ );
+
+ expect(functionMiddlewareSpan).toBeDefined();
+ expect(nestedSpan).toBeDefined();
+
+ // Both spans should be siblings under the same parent (root transaction)
+ expect(nestedSpan?.parent_span_id).toBe(functionMiddlewareSpan?.parent_span_id);
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/tunnel.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/tunnel.test.ts
new file mode 100644
index 000000000000..76a39f44be81
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/tunnel.test.ts
@@ -0,0 +1,118 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+const tunnelRouteMode =
+ process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? (process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1' ? 'custom' : 'off');
+const expectedTunnelPathMatcher =
+ tunnelRouteMode === 'static'
+ ? '/monitor'
+ : tunnelRouteMode === 'custom'
+ ? '/custom-monitor'
+ : tunnelRouteMode === 'object'
+ ? '/object-monitor'
+ : /^\/[a-z0-9]{8}$/;
+
+test.skip(tunnelRouteMode === 'off', 'Tunnel assertions only run in the tunnel-route variants');
+
+test('Sends client-side errors through the configured tunnel route', async ({ page }) => {
+ const errorEventPromise = waitForError('tanstackstart-react-static', errorEvent => {
+ return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error';
+ });
+
+ await page.goto('/');
+ const pageOrigin = new URL(page.url()).origin;
+
+ // Wait for React to hydrate (see __root.tsx) before clicking — the SSR HTML
+ // renders the button before the onClick handler is attached, and clicking
+ // pre-hydration would fire no handler and produce no error.
+ await page.locator('html[data-hydrated="true"]').waitFor();
+
+ await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible();
+
+ const managedTunnelResponsePromise = page.waitForResponse(response => {
+ const responseUrl = new URL(response.url());
+
+ return (
+ responseUrl.origin === pageOrigin &&
+ response.request().method() === 'POST' &&
+ (typeof expectedTunnelPathMatcher === 'string'
+ ? responseUrl.pathname === expectedTunnelPathMatcher
+ : expectedTunnelPathMatcher.test(responseUrl.pathname))
+ );
+ });
+
+ await page.locator('button').filter({ hasText: 'Break the client' }).click();
+
+ const managedTunnelResponse = await managedTunnelResponsePromise;
+ const managedTunnelUrl = new URL(managedTunnelResponse.url());
+ const errorEvent = await errorEventPromise;
+
+ expect(managedTunnelResponse.status()).toBe(200);
+ expect(managedTunnelUrl.origin).toBe(pageOrigin);
+
+ if (typeof expectedTunnelPathMatcher === 'string') {
+ expect(managedTunnelUrl.pathname).toBe(expectedTunnelPathMatcher);
+ } else {
+ expect(managedTunnelUrl.pathname).toMatch(expectedTunnelPathMatcher);
+ expect(managedTunnelUrl.pathname).not.toBe('/monitor');
+ }
+
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Sentry Client Test Error');
+ expect(errorEvent.transaction).toBe('/');
+});
+
+function pathnameMatchesTunnelRoute(pathname: string): boolean {
+ return typeof expectedTunnelPathMatcher === 'string'
+ ? pathname === expectedTunnelPathMatcher
+ : expectedTunnelPathMatcher.test(pathname);
+}
+
+function waitForServerHttpEvent(matchesPathname: (pathname: string) => boolean): Promise {
+ return waitForTransaction('tanstackstart-react-static', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ matchesPathname((transactionEvent.transaction ?? '').split(' ')[1] ?? '')
+ );
+ });
+}
+
+test('Does not create a server transaction for the tunnel route', async ({ page }) => {
+ // The incoming POST to the tunnel route must not be turned into an `http.server`
+ // transaction by the server SDK — tunnel traffic is plumbing, not application requests.
+ const tunnelServerEventPromise = waitForServerHttpEvent(pathnameMatchesTunnelRoute);
+
+ await page.goto('/');
+ const pageOrigin = new URL(page.url()).origin;
+
+ await page.locator('html[data-hydrated="true"]').waitFor();
+ await expect(page.locator('button').filter({ hasText: 'Break the client' })).toBeVisible();
+
+ const managedTunnelResponsePromise = page.waitForResponse(response => {
+ const responseUrl = new URL(response.url());
+
+ return (
+ responseUrl.origin === pageOrigin &&
+ response.request().method() === 'POST' &&
+ pathnameMatchesTunnelRoute(responseUrl.pathname)
+ );
+ });
+
+ await page.locator('button').filter({ hasText: 'Break the client' }).click();
+
+ // Ensure the tunnel POST was fully handled server-side before we issue the anchor request below.
+ await managedTunnelResponsePromise;
+
+ // Anchor on a regular server transaction issued *after* the tunnel POST. The Node transport flushes
+ // transactions/spans in FIFO order, so a (buggy) tunnel-route transaction would always arrive before
+ // this anchor. Racing the two lets us assert the absence of a tunnel transaction without idling on a timeout.
+ const anchorServerEventPromise = waitForServerHttpEvent(pathname => pathname.includes('/api/user/'));
+
+ await page.request.get('/api/user/456');
+
+ const winner = await Promise.race([
+ tunnelServerEventPromise.then(() => 'tunnel' as const),
+ anchorServerEventPromise.then(() => 'anchor' as const),
+ ]);
+
+ expect(winner).toBe('anchor');
+});
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json
new file mode 100644
index 000000000000..5dcdb1fa6f4a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "strictNullChecks": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.node.json
new file mode 100644
index 000000000000..97ede7ee6f2d
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts
new file mode 100644
index 000000000000..2385d8aa5e93
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts
@@ -0,0 +1,60 @@
+import { defineConfig } from 'vite';
+import tsConfigPaths from 'vite-tsconfig-paths';
+import { tanstackStart } from '@tanstack/react-start/plugin/vite';
+import viteReact from '@vitejs/plugin-react-swc';
+import { nitro } from 'nitro/vite';
+import { sentryTanstackStart } from '@sentry/tanstackstart-react/vite';
+
+const tunnelRouteMode = process.env.E2E_TEST_TUNNEL_ROUTE_MODE ?? 'off';
+const useManagedTunnelRoute = tunnelRouteMode !== 'off';
+const useCustomTunnelRoute = process.env.E2E_TEST_CUSTOM_TUNNEL_ROUTE === '1';
+
+const appDsn =
+ useManagedTunnelRoute || useCustomTunnelRoute
+ ? 'http://public@localhost:3031/1337'
+ : 'https://public@dsn.ingest.sentry.io/1337';
+
+const appTunnel = useManagedTunnelRoute
+ ? undefined
+ : useCustomTunnelRoute
+ ? '/custom-monitor'
+ : 'http://localhost:3031/';
+
+function resolveTunnelRouteOption() {
+ switch (tunnelRouteMode) {
+ case 'dynamic':
+ return true;
+ case 'static':
+ return '/monitor';
+ case 'object':
+ return { path: '/object-monitor', allowedDsns: [appDsn] };
+ default:
+ return undefined;
+ }
+}
+
+const tunnelRoute = resolveTunnelRouteOption();
+
+export default defineConfig({
+ server: {
+ port: 3000,
+ },
+ define: {
+ __APP_DSN__: JSON.stringify(appDsn),
+ __APP_TUNNEL__: appTunnel === undefined ? 'undefined' : JSON.stringify(appTunnel),
+ },
+ plugins: [
+ tsConfigPaths(),
+ tanstackStart(),
+ nitro(),
+ // react's vite plugin must come after start's vite plugin
+ viteReact(),
+ sentryTanstackStart({
+ org: process.env.E2E_TEST_SENTRY_ORG_SLUG,
+ project: process.env.E2E_TEST_SENTRY_PROJECT,
+ authToken: process.env.E2E_TEST_AUTH_TOKEN,
+ debug: true,
+ tunnelRoute,
+ }),
+ ],
+});