From e2013c9a8e17a6a73638adcb271528e3cc8132b2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 12:14:11 +0200 Subject: [PATCH 1/5] auto-wire orchestrion --- packages/react-router/package.json | 1 + packages/react-router/src/vite/plugin.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 6dc115ff8def..51c36351de8c 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -53,6 +53,7 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0", "glob": "^13.0.6" }, diff --git a/packages/react-router/src/vite/plugin.ts b/packages/react-router/src/vite/plugin.ts index 4a66a2575987..1188edabfa5b 100644 --- a/packages/react-router/src/vite/plugin.ts +++ b/packages/react-router/src/vite/plugin.ts @@ -1,3 +1,4 @@ +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; import type { ConfigEnv, Plugin } from 'vite'; import { makeConfigInjectorPlugin } from './makeConfigInjectorPlugin'; import { makeCustomSentryVitePlugins } from './makeCustomSentryVitePlugins'; @@ -21,6 +22,9 @@ export async function sentryReactRouter( plugins.push(makeConfigInjectorPlugin(options)); plugins.push(makeServerBuildCapturePlugin()); + // Injects `diagnostics_channel` publishers into instrumented server-side deps (mysql, ioredis, …) + plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); + if (process.env.NODE_ENV !== 'development' && viteConfig.command === 'build' && viteConfig.mode !== 'development') { plugins.push(makeEnableSourceMapsPlugin(options)); plugins.push(...(await makeCustomSentryVitePlugins(options))); From 6b044e0c01be9ed956c9fb2b1a0a94113ce95c1f Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 12:14:15 +0200 Subject: [PATCH 2/5] tests --- .../react-router-8-orchestrion/package.json | 1 - .../react-router-8-orchestrion/vite.config.ts | 15 ++++---- .../react-router/test/vite/plugin.test.ts | 34 +++++++++++++++++-- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json index cc390c344d24..e6bc1173db5a 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json @@ -11,7 +11,6 @@ "@react-router/node": "^8.0.0", "@react-router/serve": "^8.0.0", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "@sentry/server-utils": "file:../../packed/sentry-server-utils-packed.tgz", "ioredis": "5.10.1", "mysql": "^2.18.1" }, diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts index 494f3d41f2ce..d9fbe48672f7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts @@ -1,13 +1,10 @@ import { reactRouter } from '@react-router/dev/vite'; -import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/vite'; +import { sentryReactRouter } from '@sentry/react-router'; import { defineConfig } from 'vite'; -export default defineConfig({ - plugins: [ - reactRouter(), - // Runs the orchestrion code transform over the SSR server bundle and - // force-bundles the instrumented deps (mysql, ioredis, …) so the - // diagnostics-channel calls are actually injected at build time. - sentryOrchestrionPlugin(), - ], + +export default defineConfig(config => { + return { + plugins: [reactRouter(), sentryReactRouter({ sourcemaps: { disable: true } }, config)], + }; }); diff --git a/packages/react-router/test/vite/plugin.test.ts b/packages/react-router/test/vite/plugin.test.ts index 52306eb0dbd1..b92ebec13dea 100644 --- a/packages/react-router/test/vite/plugin.test.ts +++ b/packages/react-router/test/vite/plugin.test.ts @@ -17,11 +17,21 @@ vi.mock('../../src/vite/makeEnableSourceMapsPlugin'); vi.mock('../../src/vite/makeConfigInjectorPlugin'); vi.mock('../../src/vite/makeServerBuildCapturePlugin'); +// Stub the orchestrion plugin so these stay pure wiring tests (no apm code transformer pulled in). +// Mirror the real plugin's contract: `buildTimeInstrumentation: false` yields the inert variant. +const orchestrionVite = vi.fn((options?: { buildTimeInstrumentation?: boolean }) => ({ + name: options?.buildTimeInstrumentation === false ? 'sentry-orchestrion-disabled' : 'sentry-orchestrion-vite', +})); +vi.mock('@sentry/server-utils/orchestrion/vite', () => ({ + sentryOrchestrionPlugin: (options?: { buildTimeInstrumentation?: boolean }) => orchestrionVite(options), +})); + describe('sentryReactRouter', () => { const mockPlugins = [{ name: 'test-plugin' }]; const mockSourceMapsPlugin = { name: 'source-maps-plugin' }; const mockConfigInjectorPlugin = { name: 'sentry-config-injector' }; const mockServerBuildCapturePlugin = { name: 'sentry-react-router-server-build-capture' }; + const mockOrchestrionPlugin = { name: 'sentry-orchestrion-vite' }; beforeEach(() => { vi.clearAllMocks(); @@ -41,7 +51,7 @@ describe('sentryReactRouter', () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); @@ -51,7 +61,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin when not in build mode', async () => { const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -59,7 +69,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin in development build mode', async () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'development' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -73,6 +83,7 @@ describe('sentryReactRouter', () => { expect(result).toEqual([ mockConfigInjectorPlugin, mockServerBuildCapturePlugin, + mockOrchestrionPlugin, mockSourceMapsPlugin, ...mockPlugins, ]); @@ -102,4 +113,21 @@ describe('sentryReactRouter', () => { process.env.NODE_ENV = originalNodeEnv; }); + + it('adds the orchestrion plugin by default', async () => { + const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); + expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: undefined }); + expect(result.map(plugin => plugin?.name)).toContain('sentry-orchestrion-vite'); + }); + + it('adds an inert orchestrion plugin when `buildTimeInstrumentation` is `false`', async () => { + const result = await sentryReactRouter( + { buildTimeInstrumentation: false }, + { command: 'serve', mode: 'production' }, + ); + const pluginNames = result.map(plugin => plugin?.name); + expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: false }); + expect(pluginNames).toContain('sentry-orchestrion-disabled'); + expect(pluginNames).not.toContain('sentry-orchestrion-vite'); + }); }); From 0b7f0b302cbac462641152fef76b693fc8ce5147 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 30 Jul 2026 11:28:26 +0200 Subject: [PATCH 3/5] feat(react-router): Auto-wire orchestrion build-time instrumentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sentryReactRouter()` now adds the orchestrion bundler plugin (`@sentry/server-utils/orchestrion/vite`) automatically, so bundled SSR builds get `diagnostics_channel` publishers injected into instrumented server-side deps (mysql, ioredis, …) with no manual plugin setup. The plugin is gated on the production server build (`command === 'build'`): it force-bundles CJS deps via `ssr.noExternal`, which Vite's dev SSR server can't interop, and there is nothing to transform in dev. Cloudflare/workerd targets are out of scope for now — opt out there via the shared `buildTimeInstrumentation: false`. Migrate the orchestrion e2e coverage into `react-router-7-framework-instrumentation` and drop the standalone `react-router-8-orchestrion` app. Since bundled orchestrion is now the default, the app initializes Sentry inside the bundle (via `entry.server.tsx`) instead of a `--import` hook, so the channel subscribers land in the same bundle as the injected publishers. Fixes #22682 Co-Authored-By: Claude Opus 4.8 --- .../app/entry.server.tsx | 1 + .../app/routes.ts | 3 +- .../app/routes/performance}/db-ioredis.tsx | 7 +- .../app/routes/performance}/db-mysql.tsx | 3 +- .../app/routes/performance/redis.tsx | 22 ---- .../docker-compose.yml | 18 +++ .../global-setup.mjs | 17 ++- .../instrument.mjs | 6 +- .../package.json | 9 +- .../tests/performance/db.server.test.ts | 114 ++++++++++++++++++ .../tests/performance/redis.server.test.ts | 38 ------ .../react-router-8-orchestrion/.gitignore | 32 ----- .../react-router-8-orchestrion/app/app.css | 6 - .../app/entry.client.tsx | 23 ---- .../app/entry.server.tsx | 21 ---- .../react-router-8-orchestrion/app/root.tsx | 55 --------- .../react-router-8-orchestrion/app/routes.ts | 7 -- .../app/routes/home.tsx | 7 -- .../docker-compose.yml | 31 ----- .../global-setup.mjs | 27 ----- .../global-teardown.mjs | 12 -- .../react-router-8-orchestrion/instrument.mjs | 9 -- .../react-router-8-orchestrion/package.json | 50 -------- .../playwright.config.mjs | 16 --- .../public/favicon.ico | Bin 15086 -> 0 bytes .../react-router.config.ts | 5 - .../start-event-proxy.mjs | 6 - .../tests/db.test.ts | 88 -------------- .../react-router-8-orchestrion/tsconfig.json | 20 --- .../react-router-8-orchestrion/vite.config.ts | 10 -- packages/react-router/src/vite/plugin.ts | 10 +- .../react-router/test/vite/plugin.test.ts | 30 ++++- 32 files changed, 191 insertions(+), 512 deletions(-) rename dev-packages/e2e-tests/test-applications/{react-router-8-orchestrion/app/routes => react-router-7-framework-instrumentation/app/routes/performance}/db-ioredis.tsx (55%) rename dev-packages/e2e-tests/test-applications/{react-router-8-orchestrion/app/routes => react-router-7-framework-instrumentation/app/routes/performance}/db-mysql.tsx (78%) delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/redis.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/redis.server.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/.gitignore delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/app.css delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.client.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.server.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/root.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/home.tsx delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/docker-compose.yml delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-setup.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-teardown.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/instrument.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/playwright.config.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/public/favicon.ico delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/react-router.config.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/start-event-proxy.mjs delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tests/db.test.ts delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tsconfig.json delete mode 100644 dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/entry.server.tsx index 178a8ed4e377..b646f036b3ad 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/entry.server.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/entry.server.tsx @@ -1,3 +1,4 @@ +import '../instrument.mjs'; import { createReadableStreamFromReadable } from '@react-router/node'; import * as Sentry from '@sentry/react-router'; import { renderToPipeableStream } from 'react-dom/server'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts index 2754c153ad2f..9f0dd2325c42 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes.ts @@ -16,6 +16,7 @@ export default [ route('error-middleware', 'routes/performance/error-middleware.tsx'), route('lazy-route', 'routes/performance/lazy-route.tsx'), route('fetcher-test', 'routes/performance/fetcher-test.tsx'), - route('redis', 'routes/performance/redis.tsx'), + route('db-ioredis', 'routes/performance/db-ioredis.tsx'), + route('db-mysql', 'routes/performance/db-mysql.tsx'), ]), ] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-ioredis.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-ioredis.tsx similarity index 55% rename from dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-ioredis.tsx rename to dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-ioredis.tsx index 8774bf6d1555..ba48126c0c3d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-ioredis.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-ioredis.tsx @@ -1,9 +1,6 @@ import Redis from 'ioredis'; +import type { Route } from './+types/db-ioredis'; -// Page route (not a loader-only resource route): `@sentry/react-router` only -// renames the `http.server` transaction to the matched route (`GET /db-ioredis`) -// for rendered routes, so the orchestrion-injected ioredis spans land on a -// per-route transaction rather than the Express catch-all `GET /{*splat}`. export async function loader() { const redis = new Redis({ // Don't keep retrying forever if Redis goes away (e.g. on test teardown) @@ -20,6 +17,6 @@ export async function loader() { } } -export default function DbIoredis() { +export default function DbIoredis(_props: Route.ComponentProps) { return
db-ioredis
; } diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-mysql.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-mysql.tsx similarity index 78% rename from dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-mysql.tsx rename to dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-mysql.tsx index d7031799b70e..85fa18d37a3f 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/db-mysql.tsx +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/db-mysql.tsx @@ -1,4 +1,5 @@ import mysql from 'mysql'; +import type { Route } from './+types/db-mysql'; const connection = mysql.createConnection({ user: 'root', @@ -15,6 +16,6 @@ export function loader() { }); } -export default function DbMysql() { +export default function DbMysql(_props: Route.ComponentProps) { return
db-mysql
; } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/redis.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/redis.tsx deleted file mode 100644 index cba8275fcf63..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/app/routes/performance/redis.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Redis from 'ioredis'; -import type { Route } from './+types/redis'; - -const redis = new Redis(); - -export async function loader() { - const key = 'cache:greeting'; - await redis.set(key, 'hello from react-router'); - const value = await redis.get(key); - - return { value }; -} - -export default function RedisPage({ loaderData }: Route.ComponentProps) { - const { value } = loaderData; - return ( -
-

Redis Page

-
{value}
-
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/docker-compose.yml index a2cb7ab5f088..489862f006f7 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/docker-compose.yml +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/docker-compose.yml @@ -10,3 +10,21 @@ services: interval: 1s timeout: 3s retries: 30 + + db: + image: mysql:8.0 + restart: always + container_name: e2e-tests-react-router-7-instrumentation-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-7-framework-instrumentation/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/global-setup.mjs index aaf07f256cd3..560a7875fe60 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/global-setup.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/global-setup.mjs @@ -7,12 +7,17 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); export default async function globalSetup() { // Each run copies this app to a fresh temp dir, so `docker compose` doesn't // recognize a leftover container from a previous (e.g. interrupted) run as - // 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-7-instrumentation-redis', { stdio: 'ignore' }); - } catch { - // no stale container to remove + // part of the same project - but the container names are fixed, so the daemon + // still refuses to create new ones. Force-remove any stale leftovers first. + for (const container of [ + 'e2e-tests-react-router-7-instrumentation-redis', + 'e2e-tests-react-router-7-instrumentation-mysql', + ]) { + try { + execSync(`docker rm -f ${container}`, { stdio: 'ignore' }); + } catch { + // no stale container to remove + } } execSync('docker compose up -d --wait', { cwd: __dirname, stdio: 'inherit' }); } diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs index bf39b453fb97..d1181e268e3d 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs @@ -1,7 +1,9 @@ import * as Sentry from '@sentry/react-router'; -// Initialize Sentry early (before the server starts) -// The server instrumentations are created in entry.server.tsx +// Imported at the top of `entry.server.tsx` so it's bundled into the server build. Orchestrion injects +// its `diagnostics_channel` publishers into that same bundle, so the channel subscribers must be +// registered from within it too — a `--import` hook outside the bundle wouldn't see the inlined modules. +// The server instrumentations are created in entry.server.tsx. Sentry.init({ traceLifecycle: 'static', dsn: 'https://username@domain/123', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json index 41a5cec4c2ee..56a3736fafd2 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/package.json @@ -3,12 +3,14 @@ "version": "0.1.0", "type": "module", "private": true, + "//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels, so the orchestrion build-time transform injects the diagnostics_channel publishers.", "dependencies": { "@react-router/node": "^7", "@react-router/serve": "^7", "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "ioredis": "^5.4.1", + "ioredis": "5.10.1", "isbot": "^5.1.17", + "mysql": "^2.18.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7" @@ -17,6 +19,7 @@ "@playwright/test": "~1.56.0", "@react-router/dev": "^7", "@sentry-internal/test-utils": "link:../../../test-utils", + "@types/mysql": "^2.15.26", "@types/node": "^20", "@types/react": "18.3.1", "@types/react-dom": "18.3.1", @@ -25,8 +28,8 @@ }, "scripts": { "build": "react-router build", - "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev", - "start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js", + "dev": "react-router dev", + "start": "react-router-serve ./build/server/index.js", "proxy": "node start-event-proxy.mjs", "typecheck": "react-router typegen && tsc", "clean": "npx rimraf node_modules pnpm-lock.yaml", diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts new file mode 100644 index 000000000000..823980cfec7f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/db.server.test.ts @@ -0,0 +1,114 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +// Orchestrion injects `diagnostics_channel` publishers at build time, so these spans only exist in the +// bundled server build. `react-router dev` serves an unbundled SSR pipeline where the transform never +// runs, so the assertions are meaningless there — skip them in the dev run. +const isDev = process.env.TEST_ENV === 'development'; + +test.describe('server - orchestrion build-time db instrumentation', () => { + test.skip(isDev, 'orchestrion only injects into the bundled server build, not the dev server'); + + test('instruments ioredis automatically via orchestrion', async ({ page }) => { + const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /performance/db-ioredis' + ); + }); + + await page.goto('/performance/db-ioredis'); + + const transactionEvent = await transactionEventPromise; + const spans = transactionEvent.spans || []; + + // The server transaction must come from the native instrumentation API (not the legacy handler), + // proving the orchestrion-injected db spans share context with the React Router server span. + expect(transactionEvent.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api'); + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.redis', + description: 'set test-key [1 other arguments]', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'redis', + 'db.statement': 'set test-key [1 other arguments]', + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.redis', + description: 'get test-key', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'redis', + 'db.statement': 'get test-key', + }), + }), + ); + + // Each command maps to exactly one span (no offline-queue duplicate). + const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]'); + expect(setSpans).toHaveLength(1); + + // Every db span nests under the native instrumentation-API http.server transaction. + const rootSpanId = transactionEvent.contexts?.trace?.span_id; + const spanIds = new Set([rootSpanId, ...spans.map(span => span.span_id)]); + const dbSpans = spans.filter(span => span.op === 'db'); + expect(dbSpans.every(span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id))).toBe( + true, + ); + }); + + test('instruments mysql automatically via orchestrion', async ({ page }) => { + const transactionEventPromise = waitForTransaction(APP_NAME, transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /performance/db-mysql' + ); + }); + + await page.goto('/performance/db-mysql'); + + const transactionEvent = await transactionEventPromise; + const spans = transactionEvent.spans || []; + + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.mysql', + description: 'SELECT 1 + 1 AS solution', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT 1 + 1 AS solution', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + }), + }), + ); + expect(spans).toContainEqual( + expect.objectContaining({ + op: 'db', + origin: 'auto.db.orchestrion.mysql', + description: 'SELECT NOW()', + status: 'ok', + data: expect.objectContaining({ + 'db.system': 'mysql', + 'db.statement': 'SELECT NOW()', + 'db.user': 'root', + 'db.connection_string': expect.any(String), + 'net.peer.name': expect.any(String), + 'net.peer.port': 3306, + }), + }), + ); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/redis.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/redis.server.test.ts deleted file mode 100644 index 5b9f141195ea..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/redis.server.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; -import { APP_NAME } from '../constants'; - -test.describe('server - redis db spans (instrumentation API)', () => { - test('OTel db.redis spans nest under the native instrumentation-API http.server transaction', async ({ page }) => { - const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { - return ( - transactionEvent.transaction === 'GET /performance/redis' && - (transactionEvent.spans?.some(span => span.op === 'db.redis') ?? false) - ); - }); - - await page.goto('/performance/redis'); - - const transaction = await txPromise; - - // The server transaction must come from the native instrumentation API (not the legacy handler), - // proving auto-instrumented OTel spans still share context with the React Router server span. - expect(transaction.contexts?.trace?.op).toBe('http.server'); - expect(transaction.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api'); - - // Collect every span id in the transaction (root + children) so we can verify nesting. - const rootSpanId = transaction.contexts?.trace?.span_id; - const spanIds = new Set([rootSpanId, ...(transaction.spans ?? []).map(span => span.span_id)]); - - const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis'); - - // loader runs SET then GET => at least two redis command spans - expect(redisSpans.length).toBeGreaterThanOrEqual(2); - - // every redis span nests under the native instrumentation-API http.server transaction - const allNested = redisSpans.every( - span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id), - ); - expect(allNested).toBe(true); - }); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/.gitignore deleted file mode 100644 index ebb991370034..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/.gitignore +++ /dev/null @@ -1,32 +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 diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/app.css deleted file mode 100644 index b31c3a9d0ddf..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/app.css +++ /dev/null @@ -1,6 +0,0 @@ -html, -body { - @media (prefers-color-scheme: dark) { - color-scheme: dark; - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.client.tsx deleted file mode 100644 index 83f29c042f0b..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.client.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as Sentry from '@sentry/react-router'; -import { StrictMode, startTransition } 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-orchestrion/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.server.tsx deleted file mode 100644 index b646f036b3ad..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/entry.server.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import '../instrument.mjs'; -import { createReadableStreamFromReadable } from '@react-router/node'; -import * as Sentry from '@sentry/react-router'; -import { renderToPipeableStream } from 'react-dom/server'; -import { ServerRouter } from 'react-router'; -import { type HandleErrorFunction } from 'react-router'; - -const ABORT_DELAY = 5_000; - -const handleRequest = Sentry.createSentryHandleRequest({ - streamTimeout: ABORT_DELAY, - ServerRouter, - renderToPipeableStream, - createReadableStreamFromReadable, -}); - -export default handleRequest; - -export const handleError: HandleErrorFunction = Sentry.createSentryHandleError({ logErrors: true }); - -export const instrumentations = [Sentry.createSentryServerInstrumentation()]; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/root.tsx deleted file mode 100644 index fc20ee4f6895..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/root.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router'; -import type { Route } from './+types/root'; -import stylesheet from './app.css?url'; - -export const links: Route.LinksFunction = () => [{ rel: 'stylesheet', href: stylesheet }]; - -export function Layout({ children }: { children: React.ReactNode }) { - return ( - - - - - - - - - {children} - - - - - ); -} - -export default function App() { - return ; -} - -export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { - let message = 'Oops!'; - let details = 'An unexpected error occurred.'; - let stack: string | undefined; - - if (isRouteErrorResponse(error)) { - message = error.status === 404 ? '404' : 'Error'; - details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details; - } else if (error && error instanceof Error) { - if (import.meta.env.DEV) { - details = error.message; - stack = error.stack; - } - } - - return ( -
-

{message}

-

{details}

- {stack && ( -
-          {stack}
-        
- )} -
- ); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes.ts deleted file mode 100644 index 560647eefa08..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type RouteConfig, index, route } from '@react-router/dev/routes'; - -export default [ - index('routes/home.tsx'), - route('db-mysql', 'routes/db-mysql.tsx'), - route('db-ioredis', 'routes/db-ioredis.tsx'), -] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/home.tsx deleted file mode 100644 index 8d968568dcb7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/app/routes/home.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export function meta() { - return [{ title: 'React Router 8 Orchestrion' }]; -} - -export default function Home() { - return
home
; -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/docker-compose.yml b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/docker-compose.yml deleted file mode 100644 index f62a56205dc9..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -services: - db: - image: mysql:8.0 - restart: always - container_name: e2e-tests-react-router-8-orchestrion-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 - - redis: - image: redis:7 - restart: always - container_name: e2e-tests-react-router-8-orchestrion-redis - ports: - - '6379:6379' - healthcheck: - test: ['CMD', 'redis-cli', 'ping'] - interval: 2s - timeout: 3s - retries: 30 - start_period: 5s diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-setup.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-setup.mjs deleted file mode 100644 index fddc0ab46143..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-setup.mjs +++ /dev/null @@ -1,27 +0,0 @@ -import { execSync } from 'child_process'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -// Boot MySQL and Redis here (rather than in the `start` script) so the cold -// image pulls happen outside Playwright's webServer startup-timeout window. -// `--wait` blocks until the healthchecks in docker-compose.yml pass, so the app -// can connect immediately. -export default async function globalSetup() { - // Each run copies this app to a fresh temp dir, so `docker compose` doesn't - // recognize a leftover container from a previous (e.g. interrupted) run as - // part of the same project - but the container names are fixed, so the daemon - // still refuses to create new ones. Force-remove any stale leftovers first. - for (const container of [ - 'e2e-tests-react-router-8-orchestrion-mysql', - 'e2e-tests-react-router-8-orchestrion-redis', - ]) { - try { - execSync(`docker rm -f ${container}`, { stdio: 'ignore' }); - } catch { - // no stale container to remove - } - } - execSync('docker compose up -d --wait', { cwd: __dirname, stdio: 'inherit' }); -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/global-teardown.mjs deleted file mode 100644 index 2742279431ad..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/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-orchestrion/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/instrument.mjs deleted file mode 100644 index 67b6ca2ff092..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/instrument.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import * as Sentry from '@sentry/react-router'; - -Sentry.init({ - traceLifecycle: 'static', - dsn: 'https://username@domain/123', - environment: 'qa', // dynamic sampling bias to keep transactions - tracesSampleRate: 1.0, - tunnel: 'http://localhost:3031/', // proxy server -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json deleted file mode 100644 index e6bc1173db5a..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/package.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "react-router-8-orchestrion", - "version": "0.1.0", - "type": "module", - "private": true, - "//": "Need to use ioredis 5.10.1 because that's the last version before they support tracing channels", - "dependencies": { - "react": "^19.2.7", - "react-dom": "^19.2.7", - "react-router": "^8.0.0", - "@react-router/node": "^8.0.0", - "@react-router/serve": "^8.0.0", - "@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz", - "ioredis": "5.10.1", - "mysql": "^2.18.1" - }, - "devDependencies": { - "@types/react": "19.2.17", - "@types/react-dom": "19.2.3", - "@types/node": "^22", - "@react-router/dev": "^8.0.0", - "@playwright/test": "~1.58.0", - "@sentry-internal/test-utils": "link:../../../test-utils", - "typescript": "^5.6.3", - "vite": "^7.3.2" - }, - "scripts": { - "build": "react-router build", - "test:build-latest": "pnpm install && pnpm add react-router@latest && pnpm add @react-router/node@latest && pnpm add @react-router/serve@latest && pnpm build", - "dev": "react-router dev", - "start": "NODE_ENV=production react-router-serve ./build/server/index.js", - "proxy": "node start-event-proxy.mjs", - "clean": "npx rimraf node_modules pnpm-lock.yaml", - "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test", - "test": "playwright test" - }, - "volta": { - "extends": "../../package.json", - "node": "22.22.0" - }, - "sentryTest": { - "variants": [ - { - "build-command": "pnpm test:build-latest", - "label": "react-router-8-orchestrion (latest)" - } - ] - } -} diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/playwright.config.mjs deleted file mode 100644 index ba35892417a8..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/playwright.config.mjs +++ /dev/null @@ -1,16 +0,0 @@ -import { getPlaywrightConfig } from '@sentry-internal/test-utils'; -import { fileURLToPath } from 'url'; - -const config = getPlaywrightConfig( - { - startCommand: `PORT=3030 pnpm start`, - port: 3030, - }, - // Boot MySQL and Redis before the tests run, outside the webServer startup-timeout window. - { - globalSetup: fileURLToPath(new URL('./global-setup.mjs', import.meta.url)), - globalTeardown: fileURLToPath(new URL('./global-teardown.mjs', import.meta.url)), - }, -); - -export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/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-orchestrion/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/react-router.config.ts deleted file mode 100644 index 51e8967770b3..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/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-orchestrion/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/start-event-proxy.mjs deleted file mode 100644 index b7b1bbc72a53..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/start-event-proxy.mjs +++ /dev/null @@ -1,6 +0,0 @@ -import { startEventProxyServer } from '@sentry-internal/test-utils'; - -startEventProxyServer({ - port: 3031, - proxyServerName: 'react-router-8-orchestrion', -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tests/db.test.ts deleted file mode 100644 index 230f11e2f456..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tests/db.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -test('Instruments ioredis automatically via orchestrion', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('react-router-8-orchestrion', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-ioredis'; - }); - - await fetch(`${baseURL}/db-ioredis`); - - const transactionEvent = await transactionEventPromise; - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.orchestrion.redis', - description: 'set test-key [1 other arguments]', - status: 'ok', - data: expect.objectContaining({ - 'db.system': 'redis', - 'db.statement': 'set test-key [1 other arguments]', - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.orchestrion.redis', - description: 'get test-key', - status: 'ok', - data: expect.objectContaining({ - 'db.system': 'redis', - 'db.statement': 'get test-key', - }), - }), - ); - - // Each command maps to exactly one span (no offline-queue duplicate). - const setSpans = spans.filter(span => span.description === 'set test-key [1 other arguments]'); - expect(setSpans).toHaveLength(1); -}); - -test('Instruments mysql automatically via orchestrion', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('react-router-8-orchestrion', transactionEvent => { - return transactionEvent.contexts?.trace?.op === 'http.server' && transactionEvent.transaction === 'GET /db-mysql'; - }); - - await fetch(`${baseURL}/db-mysql`); - - const transactionEvent = await transactionEventPromise; - - const spans = transactionEvent.spans || []; - - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.orchestrion.mysql', - description: 'SELECT 1 + 1 AS solution', - status: 'ok', - data: expect.objectContaining({ - 'db.system': 'mysql', - 'db.statement': 'SELECT 1 + 1 AS solution', - 'db.user': 'root', - 'db.connection_string': expect.any(String), - 'net.peer.name': expect.any(String), - 'net.peer.port': 3306, - }), - }), - ); - expect(spans).toContainEqual( - expect.objectContaining({ - op: 'db', - origin: 'auto.db.orchestrion.mysql', - description: 'SELECT NOW()', - status: 'ok', - data: expect.objectContaining({ - 'db.system': 'mysql', - 'db.statement': 'SELECT NOW()', - 'db.user': 'root', - 'db.connection_string': expect.any(String), - 'net.peer.name': expect.any(String), - 'net.peer.port': 3306, - }), - }), - ); -}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tsconfig.json deleted file mode 100644 index a16df276e8bc..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ES2022"], - "types": ["node", "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-orchestrion/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts deleted file mode 100644 index d9fbe48672f7..000000000000 --- a/dev-packages/e2e-tests/test-applications/react-router-8-orchestrion/vite.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { reactRouter } from '@react-router/dev/vite'; -import { sentryReactRouter } from '@sentry/react-router'; -import { defineConfig } from 'vite'; - - -export default defineConfig(config => { - return { - plugins: [reactRouter(), sentryReactRouter({ sourcemaps: { disable: true } }, config)], - }; -}); diff --git a/packages/react-router/src/vite/plugin.ts b/packages/react-router/src/vite/plugin.ts index 1188edabfa5b..4b97156001b0 100644 --- a/packages/react-router/src/vite/plugin.ts +++ b/packages/react-router/src/vite/plugin.ts @@ -22,10 +22,14 @@ export async function sentryReactRouter( plugins.push(makeConfigInjectorPlugin(options)); plugins.push(makeServerBuildCapturePlugin()); - // Injects `diagnostics_channel` publishers into instrumented server-side deps (mysql, ioredis, …) - plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); - if (process.env.NODE_ENV !== 'development' && viteConfig.command === 'build' && viteConfig.mode !== 'development') { + // Injects `diagnostics_channel` publishers into instrumented server-side deps (mysql, ioredis, …) + // at build time. Only wired into the bundled server build: the plugin force-bundles CJS deps via + // `ssr.noExternal`, which Vite's dev SSR server can't interop (`exports is not defined`), and there + // is nothing to transform in dev anyway. `applyToEnvironment` further keeps it off client bundles. + // TODO: Cloudflare/workerd targets need different wiring and are skipped for now — opt out there via + // `buildTimeInstrumentation: false`. + plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); plugins.push(makeEnableSourceMapsPlugin(options)); plugins.push(...(await makeCustomSentryVitePlugins(options))); } diff --git a/packages/react-router/test/vite/plugin.test.ts b/packages/react-router/test/vite/plugin.test.ts index b92ebec13dea..52cbecceb9a9 100644 --- a/packages/react-router/test/vite/plugin.test.ts +++ b/packages/react-router/test/vite/plugin.test.ts @@ -51,7 +51,7 @@ describe('sentryReactRouter', () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); @@ -61,7 +61,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin when not in build mode', async () => { const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -69,7 +69,7 @@ describe('sentryReactRouter', () => { it('should return config injector plugin in development build mode', async () => { const result = await sentryReactRouter({}, { command: 'build', mode: 'development' }); - expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin, mockOrchestrionPlugin]); + expect(result).toEqual([mockConfigInjectorPlugin, mockServerBuildCapturePlugin]); expect(makeCustomSentryVitePlugins).not.toHaveBeenCalled(); expect(makeEnableSourceMapsPlugin).not.toHaveBeenCalled(); }); @@ -114,20 +114,38 @@ describe('sentryReactRouter', () => { process.env.NODE_ENV = originalNodeEnv; }); - it('adds the orchestrion plugin by default', async () => { - const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); + it('adds the orchestrion plugin to the server build by default', async () => { + const originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + + const result = await sentryReactRouter({}, { command: 'build', mode: 'production' }); expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: undefined }); expect(result.map(plugin => plugin?.name)).toContain('sentry-orchestrion-vite'); + + process.env.NODE_ENV = originalNodeEnv; }); it('adds an inert orchestrion plugin when `buildTimeInstrumentation` is `false`', async () => { + const originalNodeEnv = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + const result = await sentryReactRouter( { buildTimeInstrumentation: false }, - { command: 'serve', mode: 'production' }, + { command: 'build', mode: 'production' }, ); const pluginNames = result.map(plugin => plugin?.name); expect(orchestrionVite).toHaveBeenCalledWith({ buildTimeInstrumentation: false }); expect(pluginNames).toContain('sentry-orchestrion-disabled'); expect(pluginNames).not.toContain('sentry-orchestrion-vite'); + + process.env.NODE_ENV = originalNodeEnv; + }); + + it('does not add the orchestrion plugin to the dev server (serve command)', async () => { + const result = await sentryReactRouter({}, { command: 'serve', mode: 'production' }); + expect(orchestrionVite).not.toHaveBeenCalled(); + const pluginNames = result.map(plugin => plugin?.name); + expect(pluginNames).not.toContain('sentry-orchestrion-vite'); + expect(pluginNames).not.toContain('sentry-orchestrion-disabled'); }); }); From 490cdf4501aa5420ca7bf448ebd9f1c4deb069b2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 30 Jul 2026 14:05:48 +0200 Subject: [PATCH 4/5] test(react-router): Auto-wire orchestrion in react-router-8-framework e2e app Add `sentryReactRouter()` to the react-router-8-framework e2e app so the orchestrion build-time transform is exercised on React Router 8 (it previously only ran `reactRouter()`). Keeps ioredis on latest (5.11.1), where redis is instrumented via its native diagnostics channels; the existing redis `db.query` assertions still hold. Also shorten the orchestrion comment in the react-router vite plugin. Co-Authored-By: Claude Opus 4.8 --- .../react-router-8-framework/vite.config.ts | 7 +++++-- packages/react-router/src/vite/plugin.ts | 8 ++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.config.ts index 68ba30d69397..912a22c538fe 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.config.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-8-framework/vite.config.ts @@ -1,6 +1,9 @@ import { reactRouter } from '@react-router/dev/vite'; +import { sentryReactRouter } from '@sentry/react-router'; import { defineConfig } from 'vite'; -export default defineConfig({ - plugins: [reactRouter()], +export default defineConfig(config => { + return { + plugins: [reactRouter(), sentryReactRouter({ sourcemaps: { disable: true } }, config)], + }; }); diff --git a/packages/react-router/src/vite/plugin.ts b/packages/react-router/src/vite/plugin.ts index 4b97156001b0..988db02f2c45 100644 --- a/packages/react-router/src/vite/plugin.ts +++ b/packages/react-router/src/vite/plugin.ts @@ -23,12 +23,8 @@ export async function sentryReactRouter( plugins.push(makeServerBuildCapturePlugin()); if (process.env.NODE_ENV !== 'development' && viteConfig.command === 'build' && viteConfig.mode !== 'development') { - // Injects `diagnostics_channel` publishers into instrumented server-side deps (mysql, ioredis, …) - // at build time. Only wired into the bundled server build: the plugin force-bundles CJS deps via - // `ssr.noExternal`, which Vite's dev SSR server can't interop (`exports is not defined`), and there - // is nothing to transform in dev anyway. `applyToEnvironment` further keeps it off client bundles. - // TODO: Cloudflare/workerd targets need different wiring and are skipped for now — opt out there via - // `buildTimeInstrumentation: false`. + // Build-time `diagnostics_channel` injection for instrumented server deps (mysql, ioredis, …). + // Build-only: `ssr.noExternal` force-bundles CJS deps, which Vite's dev SSR can't interop. Cloudflare/workerd out of scope — opt out via `buildTimeInstrumentation: false`. plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); plugins.push(makeEnableSourceMapsPlugin(options)); plugins.push(...(await makeCustomSentryVitePlugins(options))); From 017294dd22106122f92ad8ce105efd1e25f906d7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 31 Jul 2026 16:56:04 +0200 Subject: [PATCH 5/5] pr feedback --- .../instrument.mjs | 4 -- .../tests/performance/build-injection.test.ts | 37 +++++++++++++++++++ packages/react-router/src/vite/plugin.ts | 2 - 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/build-injection.test.ts diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs index d1181e268e3d..00a6d2952286 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/instrument.mjs @@ -1,9 +1,5 @@ import * as Sentry from '@sentry/react-router'; -// Imported at the top of `entry.server.tsx` so it's bundled into the server build. Orchestrion injects -// its `diagnostics_channel` publishers into that same bundle, so the channel subscribers must be -// registered from within it too — a `--import` hook outside the bundle wouldn't see the inlined modules. -// The server instrumentations are created in entry.server.tsx. Sentry.init({ traceLifecycle: 'static', dsn: 'https://username@domain/123', diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/build-injection.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/build-injection.test.ts new file mode 100644 index 000000000000..d66ff8436650 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework-instrumentation/tests/performance/build-injection.test.ts @@ -0,0 +1,37 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { expect, test } from '@playwright/test'; + +function readBundle(dir: string): string { + const root = path.join(process.cwd(), dir); + return readdirSync(root, { recursive: true }) + .filter((file): file is string => typeof file === 'string' && file.endsWith('.js')) + .map(file => readFileSync(path.join(root, file), 'utf8')) + .join('\n'); +} + +test.describe('Orchestrion build-time injection', () => { + const serverBundle = readBundle('build/server'); + const clientBundle = readBundle('build/client'); + + test('force-bundles instrumented dependencies', () => { + expect(serverBundle).not.toMatch(/(from\s*["']mysql["']|require\(["']mysql["']\))/); + expect(serverBundle).not.toMatch(/(from\s*["']ioredis["']|require\(["']ioredis["']\))/); + }); + + test('injects diagnostics-channel publishers into the server build', () => { + expect(serverBundle).toContain('__SENTRY_ORCHESTRION__'); + expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:mysql:query["']\)/); + expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:command["']\)/); + expect(serverBundle).toMatch(/tracingChannel\(["']orchestrion:ioredis:connect["']\)/); + }); + + test('does not inject diagnostics-channel publishers into the client build', () => { + // The client build may carry the inert `__SENTRY_ORCHESTRION__.bundler = []` detection marker + // (it's environment-agnostic and harmless in a browser), but the actual `tracingChannel` publishers + // and their channel names must never leak — `applyToEnvironment` keeps the transform server-only, so + // a browser never hits a `diagnostics_channel` call that would throw. + expect(clientBundle).not.toMatch(/tracingChannel\(/); + expect(clientBundle).not.toMatch(/orchestrion:[a-z]/); + }); +}); diff --git a/packages/react-router/src/vite/plugin.ts b/packages/react-router/src/vite/plugin.ts index 988db02f2c45..602ca6f4c412 100644 --- a/packages/react-router/src/vite/plugin.ts +++ b/packages/react-router/src/vite/plugin.ts @@ -23,8 +23,6 @@ export async function sentryReactRouter( plugins.push(makeServerBuildCapturePlugin()); if (process.env.NODE_ENV !== 'development' && viteConfig.command === 'build' && viteConfig.mode !== 'development') { - // Build-time `diagnostics_channel` injection for instrumented server deps (mysql, ioredis, …). - // Build-only: `ssr.noExternal` force-bundles CJS deps, which Vite's dev SSR can't interop. Cloudflare/workerd out of scope — opt out via `buildTimeInstrumentation: false`. plugins.push(sentryOrchestrionPlugin({ buildTimeInstrumentation: options.buildTimeInstrumentation })); plugins.push(makeEnableSourceMapsPlugin(options)); plugins.push(...(await makeCustomSentryVitePlugins(options)));