Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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?
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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',
});
}
Original file line number Diff line number Diff line change
@@ -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',
});
}
Original file line number Diff line number Diff line change
@@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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',
};
Original file line number Diff line number Diff line change
@@ -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,
<StrictMode>
<StartClient />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -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');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare const __APP_DSN__: string;
declare const __APP_TUNNEL__: string | undefined;
Original file line number Diff line number Diff line change
@@ -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__,
});
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -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;
};
Original file line number Diff line number Diff line change
@@ -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 (
<RootDocument>
<Outlet />
</RootDocument>
);
}

function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
Original file line number Diff line number Diff line change
@@ -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();
}
},
},
},
});
Original file line number Diff line number Diff line change
@@ -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<Response>(resolve => {
connection.query('SELECT 1 + 1 AS solution', () => {
connection.query('SELECT NOW()', ['1', '2'], () => {
resolve(Response.json({ status: 'ok' }));
});
});
});
},
},
},
});
Original file line number Diff line number Diff line change
@@ -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');
},
},
},
});
Loading
Loading