From 1f052862fce559fd2e801256141b50c0d5981808 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 7 Aug 2026 12:50:33 +0200 Subject: [PATCH] test(react-router): Add Cloudflare Workers e2e app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers React Router on workerd: orchestrion's build-time transform injects the `diagnostics_channel` publishers and the subscriber registration into the worker bundle, and `withSentry` picks them up, so a real mysql query emits `db` spans with no runtime module hook (which workerd can't provide). Follows Cloudflare's own React Router scaffold — `@cloudflare/vite-plugin`, `workers/app.ts`, `nodejs_compat`. The nested query covers async-context restoration across mysql's socket callback. Ref: getsentry/sentry-javascript#22632 Co-Authored-By: Claude Opus 5 (1M context) --- .../react-router-8-cloudflare/.gitignore | 35 ++++++++++++ .../app/entry.client.tsx | 23 ++++++++ .../app/entry.server.tsx | 51 ++++++++++++++++++ .../react-router-8-cloudflare/app/root.tsx | 23 ++++++++ .../react-router-8-cloudflare/app/routes.ts | 6 +++ .../app/routes/home.tsx | 10 ++++ .../app/routes/performance/db-mysql.tsx | 40 ++++++++++++++ .../docker-compose.yml | 18 +++++++ .../global-setup.mjs | 13 +++++ .../global-teardown.mjs | 12 +++++ .../react-router-8-cloudflare/package.json | 45 ++++++++++++++++ .../playwright.config.mjs | 14 +++++ .../public/favicon.ico | Bin 0 -> 15086 bytes .../react-router.config.ts | 5 ++ .../start-event-proxy.mjs | 6 +++ .../tests/db.test.ts | 43 +++++++++++++++ .../react-router-8-cloudflare/tsconfig.json | 21 ++++++++ .../react-router-8-cloudflare/vite.config.ts | 13 +++++ .../react-router-8-cloudflare/workers/app.ts | 27 ++++++++++ .../react-router-8-cloudflare/wrangler.jsonc | 13 +++++ 20 files changed, 418 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore new file mode 100644 index 000000000000..e637ae342782 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts + +# react router +.react-router + +# cloudflare +.wrangler diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx new file mode 100644 index 000000000000..8fc9f5510160 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/react-router'; +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; + +Sentry.init({ + traceLifecycle: 'static', + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: 'https://username@domain/123', + tunnel: `http://localhost:3031/`, // proxy server + integrations: [Sentry.reactRouterTracingIntegration()], + tracesSampleRate: 1.0, + tracePropagationTargets: [/^\//], +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx new file mode 100644 index 000000000000..df56b2a3a8c0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx @@ -0,0 +1,51 @@ +import * as Sentry from '@sentry/react-router/cloudflare'; +import { isbot } from 'isbot'; +import { renderToReadableStream } from 'react-dom/server'; +import { type EntryContext, type HandleErrorFunction, ServerRouter } from 'react-router'; + +// workerd has no `renderToPipeableStream`, so this renders to a web stream instead. +async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, +): Promise { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + const body = await renderToReadableStream(, { + signal: request.signal, + onError(error: unknown) { + responseStatusCode = 500; + // Errors thrown after the shell has flushed can't change the status code, so surface them. + if (shellRendered) { + // eslint-disable-next-line no-console + console.error(error); + } + }, + }); + shellRendered = true; + + // Bots need complete markup rather than a streamed shell. + if (userAgent && isbot(userAgent)) { + await body.allReady; + } + + responseHeaders.set('Content-Type', 'text/html'); + + return new Response(Sentry.injectTraceMetaTags(body), { + headers: responseHeaders, + status: responseStatusCode, + }); +} + +export const handleError: HandleErrorFunction = (error, { request }) => { + // React Router aborts interrupted requests, don't report those. + if (!request.signal.aborted) { + Sentry.captureException(error); + // eslint-disable-next-line no-console + console.error(error); + } +}; + +export default Sentry.wrapSentryHandleRequest(handleRequest); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx new file mode 100644 index 000000000000..c09b53b99d46 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx @@ -0,0 +1,23 @@ +import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts new file mode 100644 index 000000000000..744ffc9485de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts @@ -0,0 +1,6 @@ +import { index, prefix, route, type RouteConfig } from '@react-router/dev/routes'; + +export default [ + index('routes/home.tsx'), + ...prefix('performance', [route('db-mysql', 'routes/performance/db-mysql.tsx')]), +] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx new file mode 100644 index 000000000000..7e59685f3987 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx @@ -0,0 +1,10 @@ +import { Link } from 'react-router'; + +export default function Home() { + return ( +
+

react-router-8-cloudflare

+ db-mysql +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx new file mode 100644 index 000000000000..ea2ac1fcea61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx @@ -0,0 +1,40 @@ +import mysql from 'mysql'; +import type { Route } from './+types/db-mysql'; + +// These queries produce `db` spans from the build-time orchestrion transform alone — workerd can't +// monkey-patch requires, so there's no OTel hook involved. +export async function loader(): Promise<{ status: string }> { + // Connect inside the loader: workerd forbids I/O in global scope. + const connection = mysql.createConnection({ + host: '127.0.0.1', + port: 3306, + user: 'root', + password: 'docker', + }); + + // Swallow socket-level errors so they don't fail the request for reasons unrelated to the spans. + connection.on('error', () => { + // no-op + }); + + try { + // The nested query runs in a fresh async context (mysql dispatches callbacks from its socket + // handler), so it only lands on this transaction if the subscriber restored the parent span. + await new Promise((resolve, reject) => { + connection.query('SELECT 1 + 1 AS solution', err1 => { + if (err1) return reject(err1); + connection.query('SELECT NOW()', err2 => { + if (err2) return reject(err2); + resolve(); + }); + }); + }); + return { status: 'ok' }; + } finally { + connection.end(); + } +} + +export default function DbMysql(_props: Route.ComponentProps) { + return
db-mysql
; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml new file mode 100644 index 000000000000..c03828e36b43 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml @@ -0,0 +1,18 @@ +services: + db: + image: mysql:8.0 + restart: always + container_name: e2e-tests-react-router-8-cloudflare-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 diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs new file mode 100644 index 000000000000..b0695e3731f7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs @@ -0,0 +1,13 @@ +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() { + // `--wait` blocks until the healthcheck passes, so the first request can connect. + execSync('docker compose up -d --wait', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs new file mode 100644 index 000000000000..2742279431ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/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/react-router-8-cloudflare/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json new file mode 100644 index 000000000000..a4ffaf1a31fc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json @@ -0,0 +1,45 @@ +{ + "name": "react-router-8-cloudflare", + "version": "0.1.0", + "type": "module", + "private": true, + "dependencies": { + "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", + "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", + "isbot": "^5.1.17", + "mysql": "^2.18.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router": "^8" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.35.0", + "@cloudflare/workers-types": "^4.20260504.0", + "@playwright/test": "~1.56.0", + "@react-router/dev": "^8", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/mysql": "^2.15.26", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "typescript": "^5.9.0", + "vite": "7.3.2", + "wrangler": "^4.120.0" + }, + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "preview": "wrangler dev --var \"E2E_TEST_DSN:$E2E_TEST_DSN\" --port 3030", + "proxy": "node start-event-proxy.mjs", + "typecheck": "react-router typegen && tsc", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm typecheck && TEST_ENV=production playwright test" + }, + "volta": { + "node": "24.15.0", + "extends": "../../package.json" + }, + "sentryTest": { + "optional": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs new file mode 100644 index 000000000000..579222812424 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs @@ -0,0 +1,14 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig( + { + startCommand: 'pnpm preview', + port: 3030, + }, + { + globalSetup: './global-setup.mjs', + globalTeardown: './global-teardown.mjs', + }, +); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dbdfcddcb14182535f6d32d1c900681321b1aa3 GIT binary patch literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts new file mode 100644 index 000000000000..51e8967770b3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts @@ -0,0 +1,5 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, +} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs new file mode 100644 index 000000000000..d78c58fa1714 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-router-8-cloudflare', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts new file mode 100644 index 000000000000..5b2acfe85b61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { + const transactionPromise = waitForTransaction('react-router-8-cloudflare', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + (transactionEvent.spans?.some(span => span.op === 'db') ?? false) + ); + }); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const transactionEvent = await transactionPromise; + const dbSpans = transactionEvent.spans!.filter(span => span.op === 'db'); + + const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + expect(firstQuery).toBeDefined(); + expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); + expect(firstQuery!.data?.['db.system']).toBe('mysql'); + expect(firstQuery!.data?.['db.statement']).toBe('SELECT 1 + 1 AS solution'); + expect(firstQuery!.data?.['net.peer.name']).toBe('127.0.0.1'); + expect(firstQuery!.data?.['net.peer.port']).toBe(3306); + expect(firstQuery!.data?.['db.user']).toBe('root'); +}); + +test('a nested query lands on the same transaction (async context restored)', async ({ request }) => { + const transactionPromise = waitForTransaction('react-router-8-cloudflare', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + (transactionEvent.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 + ); + }); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const transactionEvent = await transactionPromise; + const descriptions = transactionEvent.spans!.filter(span => span.op === 'db').map(span => span.description); + expect(descriptions).toContain('SELECT 1 + 1 AS solution'); + expect(descriptions).toContain('SELECT NOW()'); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json new file mode 100644 index 000000000000..573bcd3ad911 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + // workers-types rather than node: the server runs in workerd. + "types": ["@cloudflare/workers-types", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + }, + "include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"] +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts new file mode 100644 index 000000000000..86a05f2a7814 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts @@ -0,0 +1,13 @@ +import { cloudflare } from '@cloudflare/vite-plugin'; +import { reactRouter } from '@react-router/dev/vite'; +import { sentryReactRouter } from '@sentry/react-router'; +import { defineConfig } from 'vite'; + +export default defineConfig(async config => ({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' } }), + reactRouter(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...((await sentryReactRouter({ sourcemaps: { disable: true } }, config)) as any[]), + ], +})); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts new file mode 100644 index 000000000000..d80e3741947c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/cloudflare'; +import { createRequestHandler } from 'react-router'; + +const requestHandler = createRequestHandler(() => import('virtual:react-router/server-build'), import.meta.env.MODE); + +interface Env { + E2E_TEST_DSN: string; +} + +// `withSentry` is what reads the build-time orchestrion marker; without it the injected +// `diagnostics_channel` publishers would fire with nobody subscribed. +export default Sentry.withSentry( + (env: Env) => ({ + traceLifecycle: 'static', + dsn: env.E2E_TEST_DSN, + tunnel: 'http://localhost:3031/', + tracesSampleRate: 1.0, + environment: 'qa', // dynamic sampling bias to keep transactions + }), + { + // No load context: React Router 8 takes a `RouterContextProvider`, not v7's `{ cloudflare }` + // object, and nothing here reads bindings from a loader. + async fetch(request) { + return requestHandler(request); + }, + } satisfies ExportedHandler, +); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc new file mode 100644 index 000000000000..32521671d64e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "react-router-8-cloudflare", + "compatibility_date": "2026-06-29", + "compatibility_flags": ["nodejs_compat"], + "main": "./workers/app.ts", + "assets": { + "directory": "./build/client", + }, + "observability": { + "enabled": true, + }, +}