From 56b61be4411d7798098d5bbd7ac318cd6ce77b48 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 15:27:38 +0200 Subject: [PATCH 1/4] feat(solidstart): Auto-wire orchestrion build-time instrumentation `withSentry()` now adds the orchestrion bundler plugin to the SolidStart/Nitro build automatically, gated on the shared `buildTimeInstrumentation` opt-out. Follows Nuxt's `setupOrchestrion` pattern: push the rollup code transform and force-inline `INSTRUMENTED_MODULE_NAMES` into Nitro externals. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/solidstart/package.json | 1 + packages/solidstart/src/config/withSentry.ts | 27 ++++++++++++++++++++ packages/solidstart/src/vite/types.ts | 9 +++++++ 3 files changed, 37 insertions(+) diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 49e189b29f88..3db30b69f7cb 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -68,6 +68,7 @@ "dependencies": { "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", + "@sentry/server-utils": "10.67.0", "@sentry/solid": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index 31d8574b2a40..9163209b026c 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -1,4 +1,6 @@ import { debug } from '@sentry/core'; +import { INSTRUMENTED_MODULE_NAMES } from '@sentry/server-utils/orchestrion/config'; +import { sentryOrchestrionPlugin } from '@sentry/server-utils/orchestrion/rollup'; import type { Nitro } from 'nitropack'; import { addSentryPluginToVite } from '../vite/sentrySolidStartVite'; import type { SentrySolidStartPluginOptions } from '../vite/types'; @@ -9,6 +11,10 @@ import { } from './addInstrumentation'; import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; +// ioredis requires this CommonJS helper to be bundled with it. Leaving it +// external makes Nitro resolve the default export as a namespace object. +const IORedisDependencies = ['standard-as-callback']; + const defaultSentrySolidStartPluginOptions: Omit< SentrySolidStartPluginOptions, 'experimental_entrypointWrappedFunctions' @@ -42,9 +48,17 @@ export function withSentry( ? (...args: Parameters) => addSentryPluginToVite(viteConfig(...args), sentryPluginOptions) : addSentryPluginToVite(viteConfig, sentryPluginOptions); + const addBuildTimeInstrumentation = sentryPluginOptions.buildTimeInstrumentation !== false; + // Use a module so we don't override preset hooks. const sentryNitroModule = (nitro: Nitro) => { nitro.hooks.hook('rollup:before', async (nitro, rollupConfig) => { + if (addBuildTimeInstrumentation) { + (rollupConfig as unknown as RollupConfig).plugins.push( + sentryOrchestrionPlugin({ buildTimeInstrumentation: sentryPluginOptions.buildTimeInstrumentation }), + ); + } + if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'experimental_dynamic-import') { await addDynamicImportEntryFileWrapper({ nitro, @@ -68,11 +82,24 @@ export function withSentry( const existingModules = (server as SolidStartInlineServerConfig & { modules?: unknown[] }).modules || []; + // An externalized dependency never passes through the orchestrion code transform, so force-inline + // the instrumented modules. This has to be set statically on the Nitro config (not in a hook) + // because externalization is a resolution-time decision made before Rollup normalizes `external`. + let externals = (server as SolidStartInlineServerConfig & { externals?: { inline?: string[] } }).externals; + if (addBuildTimeInstrumentation) { + const existingInline = externals?.inline || []; + externals = { + ...externals, + inline: [...new Set([...existingInline, ...INSTRUMENTED_MODULE_NAMES, ...IORedisDependencies])], + }; + } + return { ...solidStartConfig, vite, server: { ...server, + externals, modules: [...existingModules, sentryNitroModule], }, }; diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index a01e2bde2685..088aa3a5ba26 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -126,6 +126,15 @@ export type SentrySolidStartPluginOptions = { */ debug?: boolean; + /** + * Automatic instrumentation of server-side dependencies at build time. + * + * Set to `false` to turn it off. + * + * @default true + */ + buildTimeInstrumentation?: boolean; + /** * The path to your `instrument.server.ts|js` file. * e.g. `./src/instrument.server.ts` From ab1d207fee504ed0e13f2c0e7869bc42ac44d08a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 15:27:45 +0200 Subject: [PATCH 2/4] test(solidstart): Cover orchestrion wiring in withSentry Co-Authored-By: Claude Opus 4.8 (1M context) --- .../solidstart/test/config/withSentry.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index 6a15013a524c..c9b242962c53 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -13,6 +13,17 @@ vi.mock('../../src/config/addInstrumentation', () => ({ addSentryTopImport: (...args: unknown[]) => addSentryTopImportMock(...args), })); +// Mirror the real plugin's contract: `buildTimeInstrumentation: false` yields the inert variant. +const orchestrionRollupMock = vi.fn((options?: { buildTimeInstrumentation?: boolean }) => ({ + name: options?.buildTimeInstrumentation === false ? 'sentry-orchestrion-disabled' : 'sentry-orchestrion-plugin', +})); +vi.mock('@sentry/server-utils/orchestrion/rollup', () => ({ + sentryOrchestrionPlugin: (options?: { buildTimeInstrumentation?: boolean }) => orchestrionRollupMock(options), +})); +vi.mock('@sentry/server-utils/orchestrion/config', () => ({ + INSTRUMENTED_MODULE_NAMES: ['mysql', 'ioredis'], +})); + beforeEach(() => { vi.clearAllMocks(); }); @@ -173,4 +184,47 @@ describe('withSentry()', () => { expect(modules[0]).toBe(existingModule); expect(typeof modules[1]).toBe('function'); }); + + describe('orchestrion build-time instrumentation', () => { + it('pushes the orchestrion plugin into the rollup config by default', async () => { + const config = withSentry(solidStartConfig, {}); + const { hookFn } = callSentryNitroModule(config); + const plugins: Array<{ name: string }> = []; + await hookFn(nitroOptions, { plugins }); + expect(orchestrionRollupMock).toHaveBeenCalledWith({ buildTimeInstrumentation: undefined }); + expect(plugins.map(plugin => plugin.name)).toContain('sentry-orchestrion-plugin'); + }); + + it('force-inlines the instrumented modules into server.externals by default', () => { + const config = withSentry(solidStartConfig, {}); + const externals = (config?.server as { externals?: { inline?: string[] } })?.externals; + expect(externals?.inline).toEqual(['mysql', 'ioredis', 'standard-as-callback']); + }); + + it('preserves and dedupes existing inline externals', () => { + const config = withSentry( + { + ...solidStartConfig, + server: { + ...solidStartConfig.server, + externals: { inline: ['ioredis', 'custom-dependency'] }, + }, + } as Parameters[0], + {}, + ); + const externals = (config?.server as { externals?: { inline?: string[] } })?.externals; + expect(externals?.inline).toEqual(['ioredis', 'custom-dependency', 'mysql', 'standard-as-callback']); + }); + + it('adds an inert orchestrion plugin and skips externals when buildTimeInstrumentation is false', async () => { + const config = withSentry(solidStartConfig, { buildTimeInstrumentation: false }); + const { hookFn } = callSentryNitroModule(config); + const plugins: Array<{ name: string }> = []; + await hookFn(nitroOptions, { plugins }); + expect(orchestrionRollupMock).not.toHaveBeenCalled(); + expect(plugins).toHaveLength(0); + const externals = (config?.server as { externals?: { inline?: string[] } })?.externals; + expect(externals).toBeUndefined(); + }); + }); }); From 16dd4354992015cbd3471df477212848d65e8d8d Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 15:27:56 +0200 Subject: [PATCH 3/4] test(solidstart): Add DB e2e coverage for orchestrion Extend the solidstart e2e app with mysql/ioredis routes and a Docker-backed test asserting the `auto.db.orchestrion.*` spans, proving the auto-wiring. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../solidstart/docker-compose.yml | 31 +++++++ .../solidstart/global-setup.mjs | 14 +++ .../solidstart/global-teardown.mjs | 12 +++ .../test-applications/solidstart/package.json | 4 +- .../solidstart/playwright.config.mjs | 6 +- .../solidstart/src/routes/api/db-ioredis.ts | 18 ++++ .../solidstart/src/routes/api/db-mysql.ts | 20 +++++ .../solidstart/tests/db.test.ts | 86 +++++++++++++++++++ 8 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/docker-compose.yml create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/global-setup.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/global-teardown.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-ioredis.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-mysql.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart/docker-compose.yml b/dev-packages/e2e-tests/test-applications/solidstart/docker-compose.yml new file mode 100644 index 000000000000..a55a7f9030ab --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/docker-compose.yml @@ -0,0 +1,31 @@ +services: + db: + image: mysql:8.0 + restart: always + container_name: e2e-tests-solidstart-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-solidstart-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/solidstart/global-setup.mjs b/dev-packages/e2e-tests/test-applications/solidstart/global-setup.mjs new file mode 100644 index 000000000000..cb48d539c466 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/global-setup.mjs @@ -0,0 +1,14 @@ +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() { + // Start MySQL + Redis via Docker Compose. `--wait` blocks until the + // healthchecks in docker-compose.yml pass, so the app can connect immediately. + execSync('docker compose up -d --wait', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart/global-teardown.mjs b/dev-packages/e2e-tests/test-applications/solidstart/global-teardown.mjs new file mode 100644 index 000000000000..2742279431ad --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/global-teardown.mjs @@ -0,0 +1,12 @@ +import { execSync } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export default async function globalTeardown() { + execSync('docker compose down --volumes', { + cwd: __dirname, + stdio: 'inherit', + }); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 7e382b6dc54b..fca0f9b3bd35 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -12,7 +12,9 @@ }, "type": "module", "dependencies": { - "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz" + "@sentry/solidstart": "file:../../packed/sentry-solidstart-packed.tgz", + "ioredis": "5.10.1", + "mysql": "^2.18.1" }, "devDependencies": { "@playwright/test": "~1.56.0", diff --git a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs index ee2ee42980b8..29396ea5ae66 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs @@ -5,4 +5,8 @@ const config = getPlaywrightConfig({ port: 3030, }); -export default config; +export default { + ...config, + globalSetup: './global-setup.mjs', + globalTeardown: './global-teardown.mjs', +}; diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-ioredis.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-ioredis.ts new file mode 100644 index 000000000000..eed832442755 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-ioredis.ts @@ -0,0 +1,18 @@ +import { json } from '@solidjs/router'; +import Redis from 'ioredis'; + +export async function GET() { + const redis = new Redis({ + // Don't keep retrying forever if Redis goes away (e.g. on test teardown) + maxRetriesPerRequest: 1, + retryStrategy: () => null, + }); + + try { + await redis.set('test-key', 'test-value'); + const value = await redis.get('test-key'); + return json({ value }); + } finally { + redis.disconnect(); + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-mysql.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-mysql.ts new file mode 100644 index 000000000000..c7a4dda1b288 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/routes/api/db-mysql.ts @@ -0,0 +1,20 @@ +import { json } from '@solidjs/router'; +import mysql from 'mysql'; + +export async function GET() { + const connection = mysql.createConnection({ user: 'root', password: 'docker' }); + try { + await new Promise((resolve, reject) => { + connection.query('SELECT 1 + 1 AS solution', err1 => { + if (err1) return reject(err1); + connection.query('SELECT NOW()', ['1', '2'], err2 => { + if (err2) return reject(err2); + resolve(); + }); + }); + }); + return json({ status: 'ok' }); + } finally { + connection.end(() => {}); + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts new file mode 100644 index 000000000000..cb65e36f7e8b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/db.test.ts @@ -0,0 +1,86 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Instruments ioredis automatically via build-time orchestrion', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('solidstart', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('db-ioredis') + ); + }); + + await fetch(`${baseURL}/api/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', + }), + }), + ); +}); + +test('Instruments mysql automatically via build-time orchestrion', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('solidstart', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && !!transactionEvent.transaction?.includes('db-mysql') + ); + }); + + await fetch(`${baseURL}/api/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, + }), + }), + ); +}); From b8d433988d7cd4fa9f1556aff8abe42f0fb2c867 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 29 Jul 2026 15:38:46 +0200 Subject: [PATCH 4/4] ref(solidstart): Document orchestrion rollupConfig cast Hoist the `rollup:before` config cast into one narrowing with a justification comment, replacing the duplicate double-cast. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/solidstart/src/config/withSentry.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index 9163209b026c..3caef9cdab82 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -53,8 +53,12 @@ export function withSentry( // Use a module so we don't override preset hooks. const sentryNitroModule = (nitro: Nitro) => { nitro.hooks.hook('rollup:before', async (nitro, rollupConfig) => { + // Nitro types the `rollup:before` hook's `rollupConfig.plugins` as `string[]` (plugin paths), + // but at runtime it holds resolved plugin objects we can push onto, so narrow to our own shape. + const sentryRollupConfig = rollupConfig as unknown as RollupConfig; + if (addBuildTimeInstrumentation) { - (rollupConfig as unknown as RollupConfig).plugins.push( + sentryRollupConfig.plugins.push( sentryOrchestrionPlugin({ buildTimeInstrumentation: sentryPluginOptions.buildTimeInstrumentation }), ); } @@ -62,7 +66,7 @@ export function withSentry( if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'experimental_dynamic-import') { await addDynamicImportEntryFileWrapper({ nitro, - rollupConfig: rollupConfig as unknown as RollupConfig, + rollupConfig: sentryRollupConfig, sentryPluginOptions, });