From 8dd6f21b17b9eee8042a6f3dbfa03bff87a5f09f Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 4 May 2026 14:42:57 +0200 Subject: [PATCH 1/2] test(tanstackstart): Add TanStack Start on Cloudflare Workers test app --- .../tanstackstart-react-cloudflare/.gitignore | 6 ++ .../package.json | 44 ++++++++++ .../playwright.config.ts | 6 ++ .../src/env.d.ts | 3 + .../src/router.tsx | 23 +++++ .../src/routes/__root.tsx | 52 ++++++++++++ .../src/routes/api.error.ts | 11 +++ .../src/routes/api.flush.ts | 13 +++ .../src/routes/index.tsx | 31 +++++++ .../src/routes/ssr-error.tsx | 8 ++ .../src/server.ts | 13 +++ .../start-event-proxy.mjs | 6 ++ .../tests/index.test.ts | 83 +++++++++++++++++++ .../tsconfig.json | 21 +++++ .../vite.config.ts | 12 +++ .../wrangler.jsonc | 10 +++ 16 files changed, 342 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.error.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.flush.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/ssr-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/server.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/vite.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/wrangler.jsonc diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/.gitignore b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/.gitignore new file mode 100644 index 000000000000..f67821b00540 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.output +.wrangler +.tanstack +src/routeTree.gen.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json new file mode 100644 index 000000000000..415159f7afc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json @@ -0,0 +1,44 @@ +{ + "name": "tanstackstart-react-cloudflare", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "wrangler dev --var E2E_TEST_DSN:$E2E_TEST_DSN --log-level=$(test $CI && echo 'none' || echo 'log')", + "test": "playwright test", + "typecheck": "tsc --noEmit", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm typecheck && pnpm test" + }, + "dependencies": { + "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz", + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "@sentry/core": "file:../../packed/sentry-core-packed.tgz", + "@tanstack/react-start": "^1.136.0", + "@tanstack/react-router": "^1.136.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.35.0", + "@cloudflare/workers-types": "^4.20260504.0", + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "@vitejs/plugin-react": "^4.5.0", + "typescript": "^5.9.0", + "vite": "7.3.1", + "vite-tsconfig-paths": "^5.1.4", + "wrangler": "^4.68.1" + }, + "volta": { + "node": "24.15.0", + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/playwright.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/playwright.config.ts new file mode 100644 index 000000000000..94d9558b37b6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/playwright.config.ts @@ -0,0 +1,6 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +export default getPlaywrightConfig({ + startCommand: 'pnpm preview', + port: 8787, +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/env.d.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/env.d.ts new file mode 100644 index 000000000000..eb80bafb4834 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/env.d.ts @@ -0,0 +1,3 @@ +interface Env { + E2E_TEST_DSN: string; +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx new file mode 100644 index 000000000000..c18a1a0b8167 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/router.tsx @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/browser'; +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', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, + release: 'e2e-test', + tunnel: 'http://localhost:3031/', + }); + } + + return router; +}; diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx new file mode 100644 index 000000000000..bf48b6e6cb69 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx @@ -0,0 +1,52 @@ +import type { ReactNode } from 'react'; +import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'; +import { getTraceData } from '@sentry/core'; + +export const Route = createRootRoute({ + head: () => { + const traceData = getTraceData(); + const sentryMeta = Object.entries(traceData).map(([key, value]) => ({ + name: key, + content: value, + })); + + return { + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Cloudflare E2E Test', + }, + ...sentryMeta, + ], + }; + }, + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + ); +} + +function RootDocument({ children }: Readonly<{ children: ReactNode }>) { + return ( + + + + + + {children} + + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.error.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.error.ts new file mode 100644 index 000000000000..041fb175c1f1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/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-cloudflare/src/routes/api.flush.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.flush.ts new file mode 100644 index 000000000000..b8f2313504e1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/api.flush.ts @@ -0,0 +1,13 @@ +import { createFileRoute } from '@tanstack/react-router'; +import { flush } from '@sentry/cloudflare'; + +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-cloudflare/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/index.tsx new file mode 100644 index 000000000000..5be8873475ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { createFileRoute } from '@tanstack/react-router'; + +export const Route = createFileRoute('/')({ + component: Home, +}); + +function Home() { + return ( +
+

TanStack Start Cloudflare E2E Test

+ + +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/ssr-error.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/ssr-error.tsx new file mode 100644 index 000000000000..71ba7ce92d29 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/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-cloudflare/src/server.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/server.ts new file mode 100644 index 000000000000..54bf854cba88 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/server.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/cloudflare'; +import handler from '@tanstack/react-start/server-entry'; + +export default Sentry.withSentry( + (env: Env) => ({ + dsn: env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', + tracesSampleRate: 1.0, + environment: 'qa', + }), + // @ts-expect-error - handler is not typed as a Cloudflare handler + handler, +); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/start-event-proxy.mjs new file mode 100644 index 000000000000..14ed61c3b9bd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'tanstackstart-react-cloudflare', +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts new file mode 100644 index 000000000000..3cc914b91514 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends client-side error to Sentry', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react-cloudflare', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry Client Test Error'; + }); + + await page.goto(`/`); + + await expect(page.locator('#client-error-btn')).toBeVisible(); + + await page.locator('#client-error-btn').click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values?.[0]).toEqual({ + type: 'Error', + value: 'Sentry Client Test Error', + stacktrace: expect.objectContaining({ + frames: expect.any(Array), + }), + mechanism: { + type: 'auto.browser.global_handlers.onerror', + handled: false, + }, + }); + + expect(errorEvent.transaction).toBe('/'); + expect(errorEvent.contexts?.trace?.trace_id).toEqual(expect.any(String)); + expect(errorEvent.contexts?.trace?.span_id).toEqual(expect.any(String)); +}); + +// Note: API route errors in TanStack Start are handled internally by the framework +// and don't bubble up to the Cloudflare handler. @sentry/cloudflare cannot instrument +// TanStack Start's internal error handling. For full server-side error capture, +// users would need @sentry/tanstackstart-react middleware, which isn't compatible +// with Cloudflare Workers. + +test('Sends server-side transaction for fetch request', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /'; + }); + + await fetch(`${baseURL}/`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.transaction).toBe('GET /'); + expect(transactionEvent.contexts?.trace).toEqual({ + trace_id: expect.any(String), + span_id: expect.any(String), + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + data: expect.objectContaining({ + 'sentry.origin': 'auto.http.cloudflare', + 'sentry.op': 'http.server', + }), + }); +}); + +test('Propagates trace from server to client', async ({ page }) => { + const serverTransactionPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /'; + }); + + const clientTransactionPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => { + return transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/'; + }); + + await page.goto('/'); + + const serverTransaction = await serverTransactionPromise; + const clientTransaction = await clientTransactionPromise; + + const serverTraceId = serverTransaction.contexts?.trace?.trace_id; + const clientTraceId = clientTransaction.contexts?.trace?.trace_id; + + expect(serverTraceId).toEqual(expect.any(String)); + expect(clientTraceId).toEqual(expect.any(String)); + expect(clientTraceId).toBe(serverTraceId); +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tsconfig.json new file mode 100644 index 000000000000..ecf9f5694249 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["@cloudflare/workers-types"] + }, + "include": ["src"] +} diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/vite.config.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/vite.config.ts new file mode 100644 index 000000000000..8e749133c7d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/vite.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vite'; +import tsConfigPaths from 'vite-tsconfig-paths'; +import { tanstackStart } from '@tanstack/react-start/plugin/vite'; +import viteReact from '@vitejs/plugin-react'; +import { cloudflare } from '@cloudflare/vite-plugin'; + +export default defineConfig({ + server: { + port: 3030, + }, + plugins: [cloudflare({ viteEnvironment: { name: 'ssr' } }), tsConfigPaths(), tanstackStart(), viteReact()], +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/wrangler.jsonc new file mode 100644 index 000000000000..cea7ef58657d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/wrangler.jsonc @@ -0,0 +1,10 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "tanstackstart-react-cloudflare", + "compatibility_date": "2026-05-04", + "compatibility_flags": ["nodejs_compat"], + "main": "src/server.ts", + "observability": { + "enabled": true, + }, +} From 2d2ea64e52627d5a928fbd732d86d337b7c7cd80 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 5 May 2026 18:09:57 +0200 Subject: [PATCH 2/2] fixup! test(tanstackstart): Add TanStack Start on Cloudflare Workers test app --- .../package.json | 2 +- .../src/routes/__root.tsx | 2 +- .../src/start.ts | 9 ++++++ .../tests/index.test.ts | 30 +++++++++++++++---- 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/start.ts diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json index 415159f7afc1..b5450d0e198c 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/package.json @@ -15,7 +15,7 @@ "dependencies": { "@sentry/browser": "file:../../packed/sentry-browser-packed.tgz", "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", - "@sentry/core": "file:../../packed/sentry-core-packed.tgz", + "@sentry/tanstackstart-react": "file:../../packed/sentry-tanstackstart-react-packed.tgz", "@tanstack/react-start": "^1.136.0", "@tanstack/react-router": "^1.136.0", "react": "^19.2.0", diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx index bf48b6e6cb69..bc3a376d7eba 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/__root.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react'; import { Outlet, createRootRoute, HeadContent, Scripts } from '@tanstack/react-router'; -import { getTraceData } from '@sentry/core'; +import { getTraceData } from '@sentry/tanstackstart-react'; export const Route = createRootRoute({ head: () => { diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/start.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/start.ts new file mode 100644 index 000000000000..719869f235ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/start.ts @@ -0,0 +1,9 @@ +import { sentryGlobalFunctionMiddleware, sentryGlobalRequestMiddleware } from '@sentry/tanstackstart-react'; +import { createStart } from '@tanstack/react-start'; + +export const startInstance = createStart(() => { + return { + requestMiddleware: [sentryGlobalRequestMiddleware], + functionMiddleware: [sentryGlobalFunctionMiddleware], + }; +}); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts index 3cc914b91514..30daeaedc7fd 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts @@ -31,11 +31,31 @@ test('Sends client-side error to Sentry', async ({ page }) => { expect(errorEvent.contexts?.trace?.span_id).toEqual(expect.any(String)); }); -// Note: API route errors in TanStack Start are handled internally by the framework -// and don't bubble up to the Cloudflare handler. @sentry/cloudflare cannot instrument -// TanStack Start's internal error handling. For full server-side error capture, -// users would need @sentry/tanstackstart-react middleware, which isn't compatible -// with Cloudflare Workers. +test('Sends API route error to Sentry', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react-cloudflare', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry API Route Test Error'; + }); + + await page.goto('/'); + + await expect(page.locator('#api-error-btn')).toBeVisible(); + + await page.locator('#api-error-btn').click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values?.[0]).toEqual({ + type: 'Error', + value: 'Sentry API Route Test Error', + stacktrace: expect.objectContaining({ + frames: expect.any(Array), + }), + mechanism: { + type: 'auto.middleware.tanstackstart.request', + handled: false, + }, + }); +}); test('Sends server-side transaction for fetch request', async ({ baseURL }) => { const transactionEventPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => {