From 49353293d937db8924c777f75f11b0e9c43b3983 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Mon, 7 Sep 2026 14:08:48 +0200 Subject: [PATCH 1/2] test(e2e): Keep a static-lifecycle copy of the TanStack Start React E2E app This app is about to move to span streaming. The copy stays on transaction envelopes so the static trace lifecycle remains covered for @sentry/tanstackstart-react. Tunnel-route variants stay on the original app. Co-Authored-By: Cursor Grok 4.6 --- .../tanstackstart-react-static/.gitignore | 24 +++ .../docker-compose.yml | 31 +++ .../global-setup.mjs | 14 ++ .../global-teardown.mjs | 12 ++ .../instrument.server.mjs | 13 ++ .../tanstackstart-react-static/package.json | 53 +++++ .../playwright.config.mjs | 26 +++ .../tanstackstart-react-static/src/client.tsx | 21 ++ .../src/early-side-effect.ts | 7 + .../src/globals.d.ts | 2 + .../src/instrument.client.ts | 10 + .../src/middleware.ts | 41 ++++ .../tanstackstart-react-static/src/router.tsx | 16 ++ .../src/routes/__root.tsx | 50 +++++ .../src/routes/api.db-ioredis.ts | 24 +++ .../src/routes/api.db-mysql.ts | 23 ++ .../src/routes/api.error.ts | 11 + .../src/routes/api.flush.ts | 13 ++ .../src/routes/api.test-middleware.ts | 13 ++ .../src/routes/api.user.$id.ts | 13 ++ .../src/routes/crash-before-hydration.tsx | 13 ++ .../src/routes/custom-monitor.ts | 17 ++ .../src/routes/index.tsx | 41 ++++ .../src/routes/param.$id.tsx | 14 ++ .../src/routes/ssr-error.tsx | 8 + .../src/routes/test-middleware.tsx | 86 ++++++++ .../src/routes/test-serverFn.tsx | 45 ++++ .../src/routes/users.$userId.tsx | 14 ++ .../src/routes/users.tsx | 13 ++ .../tanstackstart-react-static/src/server.ts | 12 ++ .../tanstackstart-react-static/src/start.ts | 11 + .../start-event-proxy.mjs | 6 + .../tests/db-drivers.test.ts | 98 +++++++++ .../tests/early-init.test.ts | 49 +++++ .../tests/errors.test.ts | 140 +++++++++++++ .../tests/middleware.test.ts | 197 ++++++++++++++++++ .../tests/route-parametrization.test.ts | 82 ++++++++ .../tests/trace-propagation.test.ts | 41 ++++ .../tests/transaction.test.ts | 109 ++++++++++ .../tests/tunnel.test.ts | 128 ++++++++++++ .../tanstackstart-react-static/tsconfig.json | 26 +++ .../tsconfig.node.json | 11 + .../tanstackstart-react-static/vite.config.ts | 60 ++++++ 43 files changed, 1638 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/docker-compose.yml create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-setup.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/global-teardown.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/instrument.server.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/early-side-effect.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/instrument.client.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/router.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/__root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-ioredis.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.db-mysql.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.error.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.flush.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.test-middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/api.user.$id.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/crash-before-hydration.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/custom-monitor.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/param.$id.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/ssr-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-middleware.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/test-serverFn.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.$userId.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/routes/users.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/server.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/src/start.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/db-drivers.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/early-init.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/middleware.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/route-parametrization.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/trace-propagation.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/transaction.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/tunnel.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tsconfig.node.json create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-static/vite.config.ts 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..115d46dd4785 --- /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-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-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..23908f5f3439 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json @@ -0,0 +1,53 @@ +{ + "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-streamed": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 E2E_TEST_STREAMED_SPANS=1 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 ( +
+ + + +
+ ); +} 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 ( +
+

Param: {id}

+
+ ); +} 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

+ + + + +
+ ); +} 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

+ + +
+ ); +} 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 ( +
+

User: {userId}

+
+ ); +} 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..0f9e7569ecac --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/tests/tunnel.test.ts @@ -0,0 +1,128 @@ +import { expect, test } from '@playwright/test'; +import { getSpanOp, waitForError, waitForStreamedSpan, 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 useStreamedSpans = process.env.E2E_TEST_STREAMED_SPANS === '1'; +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); +} + +// A server `http.server` transaction can arrive either as a classic transaction event (static trace +// lifecycle) or as a Span v2 segment span (`traceLifecycle: 'stream'`). These helpers wait on whichever +// format the current run produces, so the assertion below covers both lifecycles with one test body. +function waitForServerHttpEvent(matchesPathname: (pathname: string) => boolean): Promise { + if (useStreamedSpans) { + return waitForStreamedSpan('tanstackstart-react-static', span => { + return getSpanOp(span) === 'http.server' && matchesPathname((span.name ?? '').split(' ')[1] ?? ''); + }); + } + + 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, + }), + ], +}); From b3f68b34c7601d19c91dc2f7003b912a453425ee Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Mon, 7 Sep 2026 15:14:19 +0200 Subject: [PATCH 2/2] test(e2e): Drop stream leftovers from the TanStack Start static copy Co-Authored-By: Cursor Grok 4.6 --- .../tanstackstart-react-static/docker-compose.yml | 4 ++-- .../tanstackstart-react-static/package.json | 1 - .../tanstackstart-react-static/tests/tunnel.test.ts | 12 +----------- 3 files changed, 3 insertions(+), 14 deletions(-) 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 index 115d46dd4785..954690210f58 100644 --- 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 @@ -2,7 +2,7 @@ services: db: image: mysql:8.0 restart: always - container_name: e2e-tests-tanstackstart-react-mysql + 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'] @@ -20,7 +20,7 @@ services: redis: image: redis:7 restart: always - container_name: e2e-tests-tanstackstart-react-redis + container_name: e2e-tests-tanstackstart-react-static-redis ports: - '6379:6379' healthcheck: 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 index 23908f5f3439..ff237684be5d 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-static/package.json @@ -18,7 +18,6 @@ "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-streamed": "E2E_TEST_TUNNEL_ROUTE_MODE=dynamic E2E_TEST_DSN=http://public@localhost:3031/1337 E2E_TEST_STREAMED_SPANS=1 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" 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 index 0f9e7569ecac..76a39f44be81 100644 --- 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 @@ -1,9 +1,8 @@ import { expect, test } from '@playwright/test'; -import { getSpanOp, waitForError, waitForStreamedSpan, waitForTransaction } from '@sentry-internal/test-utils'; +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 useStreamedSpans = process.env.E2E_TEST_STREAMED_SPANS === '1'; const expectedTunnelPathMatcher = tunnelRouteMode === 'static' ? '/monitor' @@ -68,16 +67,7 @@ function pathnameMatchesTunnelRoute(pathname: string): boolean { : expectedTunnelPathMatcher.test(pathname); } -// A server `http.server` transaction can arrive either as a classic transaction event (static trace -// lifecycle) or as a Span v2 segment span (`traceLifecycle: 'stream'`). These helpers wait on whichever -// format the current run produces, so the assertion below covers both lifecycles with one test body. function waitForServerHttpEvent(matchesPathname: (pathname: string) => boolean): Promise { - if (useStreamedSpans) { - return waitForStreamedSpan('tanstackstart-react-static', span => { - return getSpanOp(span) === 'http.server' && matchesPathname((span.name ?? '').split(' ')[1] ?? ''); - }); - } - return waitForTransaction('tanstackstart-react-static', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' &&