From 8dd6f21b17b9eee8042a6f3dbfa03bff87a5f09f Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 4 May 2026 14:42:57 +0200 Subject: [PATCH 1/4] 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/4] 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 => { From 59f40bc8e3ae98482d236dc309842058f51f65ec Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 11 May 2026 15:21:28 +0200 Subject: [PATCH 3/4] test: Add serverFn spans | wrap with wrapFetchWithSentry --- .../src/routes/index.tsx | 14 +++ .../src/routes/test-serverFn.tsx | 47 +++++++ .../src/server.ts | 3 +- .../tests/errors.test.ts | 113 +++++++++++++++++ .../tests/index.test.ts | 103 --------------- .../tests/transaction.test.ts | 117 ++++++++++++++++++ 6 files changed, 293 insertions(+), 104 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/src/routes/test-serverFn.tsx create mode 100644 dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/errors.test.ts delete 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/tests/transaction.test.ts 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 index 5be8873475ef..5adc3a25968b 100644 --- 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 @@ -1,4 +1,9 @@ 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, @@ -17,6 +22,15 @@ function Home() { > Break the client + + + + ); +} 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 index 54bf854cba88..9ade4ffba0f2 100644 --- 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 @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/cloudflare'; +import { wrapFetchWithSentry } from '@sentry/tanstackstart-react'; import handler from '@tanstack/react-start/server-entry'; export default Sentry.withSentry( @@ -9,5 +10,5 @@ export default Sentry.withSentry( environment: 'qa', }), // @ts-expect-error - handler is not typed as a Cloudflare handler - handler, + wrapFetchWithSentry(handler), ); diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/errors.test.ts new file mode 100644 index 000000000000..db34cb3a908d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/errors.test.ts @@ -0,0 +1,113 @@ +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).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', async ({ page }) => { + const errorEventPromise = waitForError('tanstackstart-react-cloudflare', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Sentry Server Function Test Error'; + }); + + await page.goto(`/`); + + await expect(page.locator('#throw-server-fn-btn')).toBeVisible(); + + await page.locator('#throw-server-fn-btn').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, + }, + }, + ], + }, + }); +}); + +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).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Sentry API Route Test Error', + mechanism: { + type: 'auto.middleware.tanstackstart.request', + handled: false, + }, + }, + ], + }, + }); +}); + +test('Does not send SSR loader error to Sentry', async ({ baseURL, page }) => { + let errorEventOccurred = false; + + waitForError('tanstackstart-react-cloudflare', 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-cloudflare', 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-cloudflare/tests/index.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts deleted file mode 100644 index 30daeaedc7fd..000000000000 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/index.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -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)); -}); - -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 => { - 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/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts new file mode 100644 index 000000000000..f4fc2a4ee228 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts @@ -0,0 +1,117 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends a server function transaction with span from wrapFetchWithSentry', async ({ page }) => { + const transactionEventPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + ); + }); + + await page.goto('/test-serverFn'); + + await expect(page.locator('#server-fn-btn')).toBeVisible(); + + await page.locator('#server-fn-btn').click(); + + const transactionEvent = await transactionEventPromise; + + expect(Array.isArray(transactionEvent?.spans)).toBe(true); + expect(transactionEvent?.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: expect.stringContaining('GET /_serverFn/'), + op: 'function.tanstackstart', + origin: 'auto.function.tanstackstart.server', + data: expect.objectContaining({ + 'sentry.op': 'function.tanstackstart', + 'sentry.origin': 'auto.function.tanstackstart.server', + 'tanstackstart.function.hash.sha256': expect.any(String), + }), + }), + ]), + ); +}); + +test('Sends a server function transaction for a nested server function with manual span', async ({ page }) => { + const transactionEventPromise = waitForTransaction('tanstackstart-react-cloudflare', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + !!transactionEvent?.transaction?.startsWith('GET /_serverFn') + ); + }); + + await page.goto('/test-serverFn'); + + await expect(page.locator('#server-fn-nested-btn')).toBeVisible(); + + await page.locator('#server-fn-nested-btn').click(); + + const transactionEvent = await transactionEventPromise; + + expect(Array.isArray(transactionEvent?.spans)).toBe(true); + + expect(transactionEvent?.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: expect.stringContaining('GET /_serverFn/'), + op: 'function.tanstackstart', + origin: 'auto.function.tanstackstart.server', + data: expect.objectContaining({ + 'sentry.op': 'function.tanstackstart', + 'sentry.origin': 'auto.function.tanstackstart.server', + 'tanstackstart.function.hash.sha256': expect.any(String), + }), + }), + ]), + ); + + expect(transactionEvent?.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'testNestedLog', + origin: 'manual', + }), + ]), + ); +}); + +test('Sends server-side transaction for page 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).toMatchObject({ + op: 'http.server', + origin: 'auto.http.cloudflare', + status: 'ok', + }); +}); + +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).toMatch(/[a-f0-9]{32}/); + expect(clientTraceId).toMatch(/[a-f0-9]{32}/); + expect(clientTraceId).toBe(serverTraceId); +}); From 7ee2bb785d59cfae30a3aec77ae1377783467e98 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Mon, 11 May 2026 15:27:09 +0200 Subject: [PATCH 4/4] fixup! test: Add serverFn spans | wrap with wrapFetchWithSentry --- .../tests/transaction.test.ts | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts index f4fc2a4ee228..e790b49f6d37 100644 --- a/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts +++ b/dev-packages/e2e-tests/test-applications/tanstackstart-react-cloudflare/tests/transaction.test.ts @@ -17,21 +17,24 @@ test('Sends a server function transaction with span from wrapFetchWithSentry', a const transactionEvent = await transactionEventPromise; - expect(Array.isArray(transactionEvent?.spans)).toBe(true); - expect(transactionEvent?.spans).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - description: expect.stringContaining('GET /_serverFn/'), - op: 'function.tanstackstart', - origin: 'auto.function.tanstackstart.server', - data: expect.objectContaining({ - 'sentry.op': 'function.tanstackstart', - 'sentry.origin': 'auto.function.tanstackstart.server', - 'tanstackstart.function.hash.sha256': expect.any(String), - }), + expect(transactionEvent.contexts?.trace).toMatchObject({ + op: 'http.server', + origin: 'auto.http.cloudflare', + }); + + expect(transactionEvent?.spans).toHaveLength(1); + expect(transactionEvent?.spans).toEqual([ + expect.objectContaining({ + description: expect.stringContaining('GET /_serverFn/'), + op: 'function.tanstackstart', + origin: 'auto.function.tanstackstart.server', + data: expect.objectContaining({ + 'sentry.op': 'function.tanstackstart', + 'sentry.origin': 'auto.function.tanstackstart.server', + 'tanstackstart.function.hash.sha256': expect.any(String), }), - ]), - ); + }), + ]); }); test('Sends a server function transaction for a nested server function with manual span', async ({ page }) => { @@ -50,8 +53,12 @@ test('Sends a server function transaction for a nested server function with manu const transactionEvent = await transactionEventPromise; - expect(Array.isArray(transactionEvent?.spans)).toBe(true); + expect(transactionEvent.contexts?.trace).toMatchObject({ + op: 'http.server', + origin: 'auto.http.cloudflare', + }); + expect(transactionEvent?.spans).toHaveLength(2); expect(transactionEvent?.spans).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -64,11 +71,6 @@ test('Sends a server function transaction for a nested server function with manu 'tanstackstart.function.hash.sha256': expect.any(String), }), }), - ]), - ); - - expect(transactionEvent?.spans).toEqual( - expect.arrayContaining([ expect.objectContaining({ description: 'testNestedLog', origin: 'manual',