From 8e50498fae51b74e15ea749998a50a19ce394caa Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 22 Sep 2026 18:35:20 +0200 Subject: [PATCH] test(e2e): Move react-router-8-cloudflare MySQL tests into react-router-8-framework The Cloudflare variant of react-router-8-framework now covers what the separate react-router-8-cloudflare app tested, so the MySQL route and tests move over and the old app is removed. The tests now also run on Node, Bun and Deno. The Worker is wrapped by `sentryCloudflareVitePlugin` instead of a manual `withSentry` call. Co-Authored-By: Claude Opus 5 --- .../react-router-8-cloudflare/.gitignore | 35 ------------ .../app/entry.client.tsx | 22 -------- .../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 ---- .../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 15086 -> 0 bytes .../react-router.config.ts | 5 -- .../start-event-proxy.mjs | 6 -- .../tests/db.test.ts | 50 ----------------- .../react-router-8-cloudflare/tsconfig.json | 21 ------- .../react-router-8-cloudflare/vite.config.ts | 13 ----- .../react-router-8-cloudflare/workers/app.ts | 26 --------- .../react-router-8-cloudflare/wrangler.jsonc | 13 ----- .../react-router-8-framework/app/routes.ts | 1 + .../app/routes/performance/db-mysql.tsx | 4 +- .../docker-compose.yml | 17 ++++++ .../react-router-8-framework/global-setup.mjs | 2 +- .../react-router-8-framework/package.json | 4 +- .../tests/performance/mysql.server.test.ts | 53 ++++++++++++++++++ 25 files changed, 77 insertions(+), 387 deletions(-) delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/public/favicon.ico delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc rename dev-packages/e2e-tests/test-applications/{react-router-8-cloudflare => react-router-8-framework}/app/routes/performance/db-mysql.tsx (87%) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts 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 deleted file mode 100644 index e637ae342782..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# 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 deleted file mode 100644 index 0d928d68760a..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.client.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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({ - 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 deleted file mode 100644 index df56b2a3a8c0..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/entry.server.tsx +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index c09b53b99d46..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/root.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index 744ffc9485de..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes.ts +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 7e59685f3987..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/home.tsx +++ /dev/null @@ -1,10 +0,0 @@ -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/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml deleted file mode 100644 index c03828e36b43..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/docker-compose.yml +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index b0695e3731f7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-setup.mjs +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 2742279431ad..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/global-teardown.mjs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 094e2e4ebfbe..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/package.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "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.63.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.5", - "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 deleted file mode 100644 index 579222812424..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/playwright.config.mjs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 5dbdfcddcb14182535f6d32d1c900681321b1aa3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 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 deleted file mode 100644 index 51e8967770b3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/react-router.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index d78c58fa1714..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 1dfa920d1625..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tests/db.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; - -// Under span streaming the mysql span name is the query summary, so both queries here are named -// `SELECT`. `db.query.text` is what tells them apart. -test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.some(span => getSpanOp(span) === 'db'), - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - - const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); - expect(firstQuery).toBeDefined(); - expect(firstQuery!.name).toBe('SELECT'); - expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); - expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); - expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); - expect(firstQuery!.attributes['server.port']?.value).toBe(3306); - expect(firstQuery!.attributes['db.user']?.value).toBe('root'); -}); - -test('a nested query lands on the same segment (async context restored)', async ({ request }) => { - const spansPromise = collectStreamedSpans( - 'react-router-8-cloudflare', - spansOfTrace => - spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && - spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, - ); - - const res = await request.get('/performance/db-mysql'); - expect(res.status()).toBe(200); - - const spans = await spansPromise; - - // These are scoped to the http.server segment's trace, so both queries landing here is what proves - // the nested one kept the async context. - const queryTexts = spans - .filter(span => getSpanOp(span) === 'db') - .map(span => span.attributes['db.query.text']?.value); - expect(queryTexts).toContain('SELECT ? + ? AS solution'); - expect(queryTexts).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 deleted file mode 100644 index 573bcd3ad911..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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 deleted file mode 100644 index cd07d7a3fe03..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/vite.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { cloudflare } from '@cloudflare/vite-plugin'; -import { reactRouter } from '@react-router/dev/vite'; -import { sentryReactRouter } from '@sentry/react-router/vite'; -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 deleted file mode 100644 index d9a6c5451d72..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/workers/app.ts +++ /dev/null @@ -1,26 +0,0 @@ -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) => ({ - 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 deleted file mode 100644 index 32521671d64e..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/wrangler.jsonc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$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, - }, -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts index 3e3839295b1d..1e2b1b1dfd10 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes.ts @@ -20,5 +20,6 @@ export default [ route('server-action', 'routes/performance/server-action.tsx'), route('with-middleware', 'routes/performance/with-middleware.tsx'), route('redis', 'routes/performance/redis.tsx'), + 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/performance/db-mysql.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx similarity index 87% rename from dev-packages/e2e-tests/test-applications/react-router-8-cloudflare/app/routes/performance/db-mysql.tsx rename to dev-packages/e2e-tests/test-applications/react-router-8-framework/app/routes/performance/db-mysql.tsx index ea2ac1fcea61..55d73a44004c 100644 --- 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-framework/app/routes/performance/db-mysql.tsx @@ -1,8 +1,8 @@ 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. +// On Cloudflare these `db` spans come from the build-time orchestrion transform alone, because workerd +// can't monkey-patch requires. export async function loader(): Promise<{ status: string }> { // Connect inside the loader: workerd forbids I/O in global scope. const connection = mysql.createConnection({ diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml index 27367c5ae12a..dfb0c2de26c0 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/docker-compose.yml @@ -10,3 +10,20 @@ services: interval: 1s timeout: 3s retries: 30 + mysql: + image: mysql:8.0 + restart: always + container_name: e2e-tests-react-router-8-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-framework/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs index 55af1feaa14c..a7ab404749f5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/global-setup.mjs @@ -13,7 +13,7 @@ export default async function globalSetup() { // part of the same project - but the container name is fixed, so the daemon // still refuses to create a new one. Force-remove any stale leftover first. try { - execSync('docker rm -f e2e-tests-react-router-8-redis', { stdio: 'ignore' }); + execSync('docker rm -f e2e-tests-react-router-8-redis e2e-tests-react-router-8-mysql', { stdio: 'ignore' }); } catch { // no stale container to remove } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json index 55a78a9f66c7..9df6c921ef55 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/package.json @@ -12,10 +12,12 @@ "@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", "ioredis": "^5.11.1", - "isbot": "^5.1.43" + "isbot": "^5.1.43", + "mysql": "^2.18.1" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.35.0", + "@types/mysql": "^2.15.26", "@types/react": "19.2.17", "@types/react-dom": "19.2.3", "@types/node": "^22", diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts new file mode 100644 index 000000000000..20377d10c47d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/tests/performance/mysql.server.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +// Under span streaming the mysql span name is the query summary, so both queries here are named +// `SELECT`. `db.query.text` is what tells them apart. +test.describe('server - mysql db spans', () => { + test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.some(span => getSpanOp(span) === 'db'), + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT ? + ? AS solution'); + expect(firstQuery).toBeDefined(); + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); + }); + + test('a nested query lands on the same segment (async context restored)', async ({ request }) => { + const spansPromise = collectStreamedSpans( + APP_NAME, + spansOfTrace => + spansOfTrace.some(span => getSpanOp(span) === 'http.server' && span.is_segment) && + spansOfTrace.filter(span => getSpanOp(span) === 'db').length >= 2, + ); + + const res = await request.get('/performance/db-mysql'); + expect(res.status()).toBe(200); + + const spans = await spansPromise; + + // These are scoped to the http.server segment's trace, so both queries landing here is what proves + // the nested one kept the async context. + const queryTexts = spans + .filter(span => getSpanOp(span) === 'db') + .map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT ? + ? AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); + }); +});