diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/.gitignore b/dev-packages/e2e-tests/test-applications/tanstackstart-react/.gitignore new file mode 100644 index 000000000000..a547bf36d8d1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/.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/.npmrc b/dev-packages/e2e-tests/test-applications/tanstackstart-react/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs new file mode 100644 index 000000000000..8bc20de7578b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/instrument.server.mjs @@ -0,0 +1,12 @@ +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, + 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/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json new file mode 100644 index 000000000000..e44229cce78f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/package.json @@ -0,0 +1,38 @@ +{ + "name": "tanstackstart-react", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "vite build && cp instrument.server.mjs .output/server", + "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:assert": "pnpm test" + }, + "dependencies": { + "@sentry/tanstackstart-react": "latest || *", + "@tanstack/react-start": "^1.139.12", + "@tanstack/react-router": "^1.139.12", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "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.2.0", + "vite-tsconfig-paths": "^5.1.4", + "nitro": "^3.0.0", + "@playwright/test": "~1.53.2", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react/playwright.config.mjs new file mode 100644 index 000000000000..4ca3c24e7fda --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, + port: 3000, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx new file mode 100644 index 000000000000..b1c6f7727a26 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/router.tsx @@ -0,0 +1,25 @@ +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.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.tanstackRouterBrowserTracingIntegration(router)], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + tunnel: 'http://localhost:3031/', // proxy server + }); + } + + return router; +}; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx new file mode 100644 index 000000000000..0a268a350e34 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/__root.tsx @@ -0,0 +1,42 @@ +import 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() { + return ( + + + + ); +} + +function RootDocument({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {children} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.error.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.error.ts new file mode 100644 index 000000000000..470d53346ad7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/api.error.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/tanstackstart-react'; +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/api/error')({ + server: { + handlers: { + GET: async () => { + try { + throw new Error('Sentry API Route Test Error'); + } catch (error) { + Sentry.captureException(error); + throw error; + } + }, + }, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react/src/routes/index.tsx new file mode 100644 index 000000000000..a92fabf20c4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/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/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react/start-event-proxy.mjs new file mode 100644 index 000000000000..a3f8045010bd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'tanstackstart-react', +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts new file mode 100644 index 000000000000..f5d25febb7a4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tests/errors.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends client-side error to Sentry with auto-instrumentation', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error'; + }); + + await page.goto(`/`); + + 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', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry Server Function Test Error'; + }); + + 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: { + handled: false, + }, + }, + ], + }, + }); + + expect(errorEvent.transaction).toBe('/'); +}); + +test('Sends API route error to Sentry if manually instrumented', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry API Route Test Error'; + }); + + 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: { + handled: true, + }, + }, + ], + }, + }); + + expect(errorEvent.transaction).toBe('GET /api/error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react/tsconfig.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tsconfig.json new file mode 100644 index 000000000000..5dcdb1fa6f4a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/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/tsconfig.node.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react/tsconfig.node.json new file mode 100644 index 000000000000..97ede7ee6f2d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/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/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts new file mode 100644 index 000000000000..4df9fbb14208 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react/vite.config.ts @@ -0,0 +1,18 @@ +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'; + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths(), + tanstackStart(), + nitro(), + // react's vite plugin must come after start's vite plugin + viteReact(), + ], +});