From 39428c8b286e4d54410e91cb733b55b08c0aa401 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 14:52:43 +0200 Subject: [PATCH 1/4] feat(server-runtime-injection): Split runtime orchestrion injection into a dedicated package The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the vendored transformer chain meriyah/esquery/astring/source-map/tracing-hooks) must stay EXTERNAL when apps bundle their server, while the rest of `@sentry/server-utils` (barrel + config + bundler plugins) is meant to be bundled. Sharing one package made externalization fiddly. This moves the runtime injection into a new package, `@sentry/server-runtime-injection`, so "keep this external" is a clean package boundary. - New package holds `register`/`hook`/`import-hook` (subpaths `/register`, `/hook`, `/import-hook`) and vendors the transformer chain; it depends on `@sentry/server-utils` and imports `SENTRY_INSTRUMENTATIONS` from `./orchestrion/config` (config stays put). - `@sentry/server-utils` drops the runtime dir/exports and the runtime-only vendored deps; the lone `config/index.ts` bundler re-export moves to a new `./orchestrion/bundler-transforms` subpath so importing config stays transformer-free (bun updated). - References updated: node SDK + test mock, deno import, the shared `--import` template + `makeOrchestrionLoader` guard, Next.js externalization (`ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES`, nextjs-anchored require-ability check, forwarder generation) + tests, `.size-limit.js`. - Dep added to runtime-injection consumers (node, nextjs, deno, aws/gcp-serverless, astro, tanstackstart-react); package registered in workspaces, `.craft.yml`, CODEOWNERS, aws e2e. Stacked on the detect/warn PR (#23675). Co-Authored-By: Claude Opus 4.8 (1M context) --- .craft.yml | 4 + .github/CODEOWNERS | 1 + .size-limit.js | 2 +- .../aws-serverless/src/stack.ts | 4 +- .../rollup-utils/code/importHookTemplate.js | 2 +- dev-packages/rollup-utils/npmHelpers.mjs | 8 +- package.json | 1 + packages/astro/package.json | 1 + packages/aws-serverless/package.json | 1 + packages/bun/src/plugin.ts | 4 +- packages/core/src/utils/worldwide.ts | 2 +- packages/deno/package.json | 1 + packages/deno/src/import.mjs | 2 +- packages/google-cloud-serverless/package.json | 1 + packages/nextjs/package.json | 1 + packages/nextjs/scripts/buildRollup.ts | 18 +-- .../src/config/diagnosticsChannelInjection.ts | 39 ++++-- .../diagnosticsChannelInjection.test.ts | 35 +++--- .../webpack/constructWebpackConfig.test.ts | 4 +- packages/node/README.md | 8 +- packages/node/package.json | 1 + packages/node/src/sdk/index.ts | 2 +- .../sdk/diagnosticsChannelInjection.test.ts | 2 +- packages/nuxt/README.md | 2 +- packages/server-runtime-injection/README.md | 22 ++++ .../server-runtime-injection/package.json | 94 ++++++++++++++ .../rollup.npm.config.mjs | 116 ++++++++++++++++++ .../src}/apm-js-collab-tracing-hooks.d.ts | 0 .../src}/hook.mjs | 2 +- .../src}/import-hook.mjs | 4 +- .../src}/register.ts | 25 ++-- .../test}/register.test.ts | 2 +- .../server-runtime-injection/tsconfig.json | 12 ++ .../tsconfig.test.json | 10 ++ .../tsconfig.types.json | 10 ++ .../server-runtime-injection/vite.config.ts | 9 ++ packages/server-utils/package.json | 24 ++-- packages/server-utils/rollup.npm.config.mjs | 46 ++----- .../src/orchestrion/config/index.ts | 5 - .../test/orchestrion/bundler.test.ts | 9 +- packages/server-utils/tsconfig.json | 9 +- packages/tanstackstart-react/package.json | 1 + 42 files changed, 402 insertions(+), 144 deletions(-) create mode 100644 packages/server-runtime-injection/README.md create mode 100644 packages/server-runtime-injection/package.json create mode 100644 packages/server-runtime-injection/rollup.npm.config.mjs rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/apm-js-collab-tracing-hooks.d.ts (100%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/hook.mjs (92%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/import-hook.mjs (88%) rename packages/{server-utils/src/orchestrion/runtime => server-runtime-injection/src}/register.ts (89%) rename packages/{server-utils/test/orchestrion => server-runtime-injection/test}/register.test.ts (97%) create mode 100644 packages/server-runtime-injection/tsconfig.json create mode 100644 packages/server-runtime-injection/tsconfig.test.json create mode 100644 packages/server-runtime-injection/tsconfig.types.json create mode 100644 packages/server-runtime-injection/vite.config.ts diff --git a/.craft.yml b/.craft.yml index 35e3968c7a8f..c5e537806bcb 100644 --- a/.craft.yml +++ b/.craft.yml @@ -12,6 +12,10 @@ targets: - name: npm id: '@sentry/server-utils' includeNames: /^sentry-server-utils-\d.*\.tgz$/ + # Depends on @sentry/server-utils (for the shared instrumentation config); publish after it. + - name: npm + id: '@sentry/server-runtime-injection' + includeNames: /^sentry-server-runtime-injection-\d.*\.tgz$/ ## 1.3 Browser Utils package - name: npm id: '@sentry/browser-utils' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 19853fb40696..4eb40336293b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,7 @@ # Node/server runtimes and related packages # TEMP: whole JS SDK team reviews orchestrion work; revert to team-javascript-sdks-server after /packages/node/ @getsentry/team-javascript-sdks +/packages/server-runtime-injection/ @getsentry/team-javascript-sdks /packages/server-utils/ @getsentry/team-javascript-sdks /packages/node-native/ @getsentry/team-javascript-sdks-server /packages/profiling-node/ @getsentry/team-javascript-sdks-server diff --git a/.size-limit.js b/.size-limit.js index 6ad5eb831484..635c316bea82 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -411,7 +411,7 @@ module.exports = [ }, { name: '@sentry/node/import (ESM hook with diagnostics-channel injection)', - path: ['packages/server-utils/build/esm/orchestrion/runtime/hook.js', 'packages/node/build/import-hook.mjs'], + path: ['packages/server-runtime-injection/build/esm/hook.js', 'packages/node/build/import-hook.mjs'], ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, limit: '91 KB', diff --git a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts index 1d56430e0575..87b5d2b7f8c3 100644 --- a/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts +++ b/dev-packages/e2e-tests/test-applications/aws-serverless/src/stack.ts @@ -55,14 +55,14 @@ export class LocalLambdaStack extends Stack { const packageLockPath = path.join(lambdaPath, 'package-lock.json'); const nodeModulesPath = path.join(lambdaPath, 'node_modules'); - // `dir` is the package directory under `packages/`; `name` is the published - // npm name (most are `@sentry/`, but `server-utils` is `@sentry-internal`). + // `dir` is the package directory under `packages/`; `name` is the published npm name. const packagesToLink: Array<{ dir: string; name: string }> = [ { dir: 'aws-serverless', name: '@sentry/aws-serverless' }, { dir: 'node', name: '@sentry/node' }, { dir: 'core', name: '@sentry/core' }, { dir: 'opentelemetry', name: '@sentry/opentelemetry' }, { dir: 'server-utils', name: '@sentry/server-utils' }, + { dir: 'server-runtime-injection', name: '@sentry/server-runtime-injection' }, { dir: 'bundler-plugins', name: '@sentry/bundler-plugins' }, ]; const dependencies: Record = {}; diff --git a/dev-packages/rollup-utils/code/importHookTemplate.js b/dev-packages/rollup-utils/code/importHookTemplate.js index 590a81734c10..950f5d6f527e 100644 --- a/dev-packages/rollup-utils/code/importHookTemplate.js +++ b/dev-packages/rollup-utils/code/importHookTemplate.js @@ -1 +1 @@ -import '@sentry/server-utils/orchestrion/import-hook'; +import '@sentry/server-runtime-injection/import-hook'; diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 064dd6f4e8e9..8668f960a1a3 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -192,9 +192,9 @@ export function makeNPMConfigVariants(baseConfig, options = {}) { /** * Emits the `@sentry//import` entry (`build/import-hook.mjs`) as part of the rollup build, * used as `node --import @sentry//import app.js`. The generated hook imports - * `@sentry/server-utils/orchestrion/import-hook`, which registers the orchestrion - * diagnostics-channel injection, so the consuming package must declare `@sentry/server-utils` as a - * dependency. + * `@sentry/server-runtime-injection/import-hook`, which registers the orchestrion + * diagnostics-channel injection, so the consuming package must declare + * `@sentry/server-runtime-injection` as a dependency. * * @param {string} outputFolder Build output folder. */ @@ -209,7 +209,7 @@ export function makeOrchestrionLoader(outputFolder) { ); } - const requiredDep = '@sentry/server-utils'; + const requiredDep = '@sentry/server-runtime-injection'; const foundRequiredDep = Object.keys(packageDotJSON.dependencies ?? {}).some(key => { return key === requiredDep; diff --git a/package.json b/package.json index 4c88d266077b..a25b4dd72738 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "packages/replay-internal", "packages/replay-canvas", "packages/replay-worker", + "packages/server-runtime-injection", "packages/server-utils", "packages/solid", "packages/solidstart", diff --git a/packages/astro/package.json b/packages/astro/package.json index e58785f37a14..f7c3f35da64b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -55,6 +55,7 @@ "@sentry/core": "10.67.0", "@sentry/conventions": "^0.20.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index c4c0fe8711ab..c957bab5f64c 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -59,6 +59,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@types/aws-lambda": "^8.10.161" }, diff --git a/packages/bun/src/plugin.ts b/packages/bun/src/plugin.ts index 7c0e3abf3637..956ee611ac8b 100644 --- a/packages/bun/src/plugin.ts +++ b/packages/bun/src/plugin.ts @@ -38,9 +38,11 @@ type UnknownPlugin = any; // module system. import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; import { - INSTRUMENTED_MODULE_NAMES, moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER, +} from '@sentry/server-utils/orchestrion/bundler-transforms'; +import { + INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS, withoutInstrumentedExternals, } from '@sentry/server-utils/orchestrion/config'; diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index ca88dd225db6..e26e9db8933d 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -80,7 +80,7 @@ export type InternalGlobal = { /** * Set once `registerDiagnosticsChannelInjection()` has run but could not * install the runtime module hooks — most commonly because - * `@sentry/server-utils` was bundled into the app (which strips its vendored + * `@sentry/server-runtime-injection` was bundled into the app (which strips its vendored * code transformer) or the Node runtime lacks the required module-hook API. * Dedupes the one-time warning and short-circuits repeat calls. */ diff --git a/packages/deno/package.json b/packages/deno/package.json index f68c05a6dc28..25541f23eee6 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -30,6 +30,7 @@ "@opentelemetry/api": "^1.9.1", "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0" }, "scripts": { diff --git a/packages/deno/src/import.mjs b/packages/deno/src/import.mjs index dd501c169a8f..3db393b646c1 100644 --- a/packages/deno/src/import.mjs +++ b/packages/deno/src/import.mjs @@ -12,4 +12,4 @@ * * @module */ -import '@sentry/server-utils/orchestrion/import-hook'; +import '@sentry/server-runtime-injection/import-hook'; diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index ca9486f02f30..b36eb35e039d 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -39,6 +39,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0" }, "devDependencies": { diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3d1e1d11d42a..2bfbefc83692 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -81,6 +81,7 @@ "@sentry/node": "10.67.0", "@sentry/opentelemetry": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/vercel-edge": "10.67.0", "rollup": "^4.60.3", diff --git a/packages/nextjs/scripts/buildRollup.ts b/packages/nextjs/scripts/buildRollup.ts index 69ba523ecb23..56f16f972549 100644 --- a/packages/nextjs/scripts/buildRollup.ts +++ b/packages/nextjs/scripts/buildRollup.ts @@ -24,28 +24,28 @@ fs.readdirSync(esmTemplateDir).forEach(templateFile => ); // Generate the orchestrion runtime forwarders (see `src/config/diagnosticsChannelInjection.ts`) -// from `@sentry/server-utils`' own exports map, so a new subpath there is forwarded automatically. -// Only `require`-able entries get one, since the emitted external is a `require()`. Written as -// plain CJS, not built by rollup: they are loaded by specifier, never bundled. -const SERVER_UTILS = '@sentry/server-utils'; +// from `@sentry/server-runtime-injection`' own exports map, so a new subpath there is forwarded +// automatically. Only `require`-able entries get one, since the emitted external is a `require()`. +// Written as plain CJS, not built by rollup: they are loaded by specifier, never bundled. +const RUNTIME_INJECTION = '@sentry/server-runtime-injection'; const orchestrionRuntimeBuildDir = 'build/orchestrion-runtime'; -const serverUtilsExports = ( - JSON.parse(fs.readFileSync(require.resolve(`${SERVER_UTILS}/package.json`), 'utf8')) as { +const runtimeInjectionExports = ( + JSON.parse(fs.readFileSync(require.resolve(`${RUNTIME_INJECTION}/package.json`), 'utf8')) as { exports: Record; } ).exports; -for (const [key, conditions] of Object.entries(serverUtilsExports)) { +for (const [key, conditions] of Object.entries(runtimeInjectionExports)) { if (key === './package.json' || typeof conditions === 'string' || !conditions.require) { continue; } - // '.' → 'index', './orchestrion/register' → 'orchestrion/register' + // '.' → 'index', './register' → 'register' const forwarderPath = path.join(orchestrionRuntimeBuildDir, `${key === '.' ? 'index' : key.slice(2)}.js`); fs.mkdirSync(path.dirname(forwarderPath), { recursive: true }); fs.writeFileSync( forwarderPath, - `// Generated by scripts/buildRollup.ts — do not edit.\nmodule.exports = require('${SERVER_UTILS}${key.slice(1)}');\n`, + `// Generated by scripts/buildRollup.ts — do not edit.\nmodule.exports = require('${RUNTIME_INJECTION}${key.slice(1)}');\n`, ); } diff --git a/packages/nextjs/src/config/diagnosticsChannelInjection.ts b/packages/nextjs/src/config/diagnosticsChannelInjection.ts index 3040fc6041dc..36939c446f2b 100644 --- a/packages/nextjs/src/config/diagnosticsChannelInjection.ts +++ b/packages/nextjs/src/config/diagnosticsChannelInjection.ts @@ -1,4 +1,4 @@ -import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestrion/webpack'; +import { createRequire } from 'node:module'; /** * Instrumented packages verified (via e2e) to bundle correctly, removed from Sentry's own @@ -9,17 +9,38 @@ import { resolveOrchestrionRuntimeRequest } from '@sentry/server-utils/orchestri export const BUNDLE_SAFE_INSTRUMENTED_PACKAGES = ['ioredis']; /** - * `@sentry/server-utils` (where `register.ts` and the bundled orchestrion runtime ship) must stay - * external: `register.ts` passes its own `__filename`/`import.meta.url` as the `parentURL` for - * `Module.register('@sentry/server-utils/orchestrion/hook.mjs', …)`, so that self-reference only + * `@sentry/server-runtime-injection` (where `register.ts` and the bundled orchestrion runtime ship) + * must stay external: `register.ts` passes its own `__filename`/`import.meta.url` as the `parentURL` + * for `Module.register('@sentry/server-runtime-injection/hook', …)`, so that self-reference only * resolves while the code still lives at its real `node_modules` location. Bundled into an app * server chunk instead, the specifier would have to resolve from the chunk's output location, * which fails under isolated installs (pnpm) where the package is a transitive dependency. * - * (The `@apm-js-collab/*` packages no longer appear here: they are bundled into - * `@sentry/server-utils`' build, so no import of them exists at runtime.) + * `@sentry/server-utils` (the barrel + bundler plugins) is NOT here — it is meant to be bundled; the + * build-time snippet's `@sentry/server-utils` import is handled separately by the code-transform. */ -export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = ['@sentry/server-utils']; +export const ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES = ['@sentry/server-runtime-injection']; + +// `require` anchored at THIS package (`@sentry/nextjs`), which depends on +// `@sentry/server-runtime-injection` — so the resolvability check below works even under isolated +// installs (pnpm), where a resolver anchored at `@sentry/server-utils` could not see it. +let nextjsRequire: NodeJS.Require; +/*! rollup-include-cjs-only */ +nextjsRequire = createRequire(__filename); +/*! rollup-include-cjs-only-end */ +/*! rollup-include-esm-only */ +nextjsRequire = createRequire(import.meta.url); +/*! rollup-include-esm-only-end */ + +/** Whether `request` resolves as a `require`-able module (skips ESM-only subpaths like `/hook`). */ +function isRequireResolvable(request: string): boolean { + try { + nextjsRequire.resolve(request); + return true; + } catch { + return false; + } +} /** Remove the given packages from a `serverExternalPackages` list. */ export function filterInstrumentedExternals(externals: string[], packagesToBundle: string[]): string[] { @@ -28,7 +49,7 @@ export function filterInstrumentedExternals(externals: string[], packagesToBundl } /** - * Where the generated forwarders live — one CJS one-liner per `@sentry/server-utils` entrypoint + * Where the generated forwarders live — one CJS one-liner per `@sentry/server-runtime-injection` entrypoint * (see `scripts/buildRollup.ts`). Forwarding through `@sentry/nextjs`, always a direct dependency, * is what makes the emitted specifier both resolvable from `.next/server/**` and relocation-safe. */ @@ -67,7 +88,7 @@ export async function externalizeOrchestrionRuntimePackages({ } // Not `require`-able (ESM-only subpath, or a typo): webpack reports it better than we can. - if (!resolveOrchestrionRuntimeRequest(request)) { + if (!isRequireResolvable(request)) { return undefined; } diff --git a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts index 4790ce65d540..effe24b8bef8 100644 --- a/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts +++ b/packages/nextjs/test/config/diagnosticsChannelInjection.test.ts @@ -40,8 +40,8 @@ describe('getServerExternalPackagesPatch (build-time instrumentation)', () => { expect(externals).toContain('mysql'); expect(externals).toContain('pg'); expect(externals).toContain('pg-pool'); - // The orchestrion machinery must be external for the runtime hook to work. - expect(externals).toContain('@sentry/server-utils'); + // The runtime injection package must be external for the runtime hook to work. + expect(externals).toContain('@sentry/server-runtime-injection'); }); it('respects user-provided externals even for bundle-safe packages', () => { @@ -61,10 +61,8 @@ describe('getServerExternalPackagesPatch (build-time instrumentation)', () => { describe('externalizeOrchestrionRuntimePackages', () => { // An absolute path here breaks every deploy that relocates the output, so it has to stay bare. it.each([ - ['@sentry/server-utils', '@sentry/nextjs/orchestrion-runtime/index'], - ['@sentry/server-utils/orchestrion/config', '@sentry/nextjs/orchestrion-runtime/orchestrion/config'], - ['@sentry/server-utils/orchestrion/register', '@sentry/nextjs/orchestrion-runtime/orchestrion/register'], - ['@sentry/server-utils/orchestrion/webpack', '@sentry/nextjs/orchestrion-runtime/orchestrion/webpack'], + ['@sentry/server-runtime-injection', '@sentry/nextjs/orchestrion-runtime/index'], + ['@sentry/server-runtime-injection/register', '@sentry/nextjs/orchestrion-runtime/register'], ])('externalizes %s as the relocatable bare specifier %s', async (request, expected) => { const external = await externalizeOrchestrionRuntimePackages({ request }); @@ -72,19 +70,19 @@ describe('externalizeOrchestrionRuntimePackages', () => { expect(isAbsolute(expected)).toBe(false); }); - it('ignores the bundled @apm-js-collab packages — no import of them exists in the dist anymore', async () => { + it('does not externalize @sentry/server-utils — it is meant to be bundled', async () => { await expect( - externalizeOrchestrionRuntimePackages({ request: '@apm-js-collab/tracing-hooks' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/orchestrion/config' }), ).resolves.toBeUndefined(); }); // A `commonjs` external could never load these, so webpack gets to report them instead. - it('ignores subpaths @sentry/server-utils does not expose to require()', async () => { + it('ignores subpaths @sentry/server-runtime-injection does not expose to require()', async () => { await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/orchestrion/hook' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection/hook' }), ).resolves.toBeUndefined(); await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils/does-not-exist' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection/does-not-exist' }), ).resolves.toBeUndefined(); }); @@ -92,27 +90,28 @@ describe('externalizeOrchestrionRuntimePackages', () => { await expect(externalizeOrchestrionRuntimePackages({ request: 'some-other-package' })).resolves.toBeUndefined(); // Prefix matching must not leak beyond a package-name boundary. await expect( - externalizeOrchestrionRuntimePackages({ request: '@sentry/server-utils-extras' }), + externalizeOrchestrionRuntimePackages({ request: '@sentry/server-runtime-injection-extras' }), ).resolves.toBeUndefined(); await expect(externalizeOrchestrionRuntimePackages({})).resolves.toBeUndefined(); }); }); // Exercises the generated artifacts, so it needs the package built — as does this file's import of -// `@sentry/server-utils`. +// `@sentry/server-runtime-injection`. describe('orchestrion runtime forwarders (generated)', () => { const nodeRequire = createRequire(import.meta.url); const forwarderDir = fileURLToPath(new URL('../../build/orchestrion-runtime/', import.meta.url)); - /** Every `@sentry/server-utils` entrypoint that a `commonjs` external could load. */ + /** Every `@sentry/server-runtime-injection` entrypoint that a `commonjs` external could load. */ const requireableSubpaths = Object.entries( - (nodeRequire('@sentry/server-utils/package.json') as { exports: Record }).exports, + (nodeRequire('@sentry/server-runtime-injection/package.json') as { exports: Record }) + .exports, ) .filter(([key, conditions]) => key !== './package.json' && conditions.require) .map(([key]) => key); it.each(requireableSubpaths)('generates a forwarder for %s that re-exports it unchanged', subpath => { - const request = `@sentry/server-utils${subpath.slice(1)}`; + const request = `@sentry/server-runtime-injection${subpath.slice(1)}`; const forwarderFile = `${forwarderDir}${subpath === '.' ? 'index' : subpath.slice(2)}.js`; expect(existsSync(forwarderFile)).toBe(true); @@ -122,8 +121,8 @@ describe('orchestrion runtime forwarders (generated)', () => { // The emitted specifier must resolve the way it will from a chunk: through the package exports. it.each(requireableSubpaths)('exposes the forwarder for %s through the package exports', subpath => { const specifier = getOrchestrionForwarderSpecifier( - `@sentry/server-utils${subpath.slice(1)}`, - '@sentry/server-utils', + `@sentry/server-runtime-injection${subpath.slice(1)}`, + '@sentry/server-runtime-injection', ); expect(nodeRequire.resolve(specifier)).toBe(`${forwarderDir}${subpath === '.' ? 'index' : subpath.slice(2)}.js`); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 96eb751bc697..13371b1ce170 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -759,8 +759,8 @@ describe('constructWebpackConfigFunction()', () => { const externals = finalWebpackConfig.externals as ((data: { request?: string }) => Promise)[]; expect(Array.isArray(externals)).toBe(true); - await expect(externals[0]({ request: '@sentry/server-utils/orchestrion/register' })).resolves.toBe( - 'commonjs @sentry/nextjs/orchestrion-runtime/orchestrion/register', + await expect(externals[0]({ request: '@sentry/server-runtime-injection/register' })).resolves.toBe( + 'commonjs @sentry/nextjs/orchestrion-runtime/register', ); await expect(externals[0]({ request: 'some-other-package' })).resolves.toBeUndefined(); }); diff --git a/packages/node/README.md b/packages/node/README.md index a2a32c69ab9f..28fe7f0bd68f 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -75,18 +75,18 @@ NODE_OPTIONS="--import ./instrument.mjs" npm run start ### Bundling your server `@sentry/node` installs its automatic (diagnostics-channel) instrumentation through a runtime module -hook that ships in `@sentry/server-utils` and is designed to run from `node_modules`. There are two +hook that ships in `@sentry/server-runtime-injection` and is designed to run from `node_modules`. There are two supported ways to keep auto-instrumentation working when you bundle your server: -1. **Keep `@sentry/server-utils` external** (do not inline it into the bundle) so the runtime hook +1. **Keep `@sentry/server-runtime-injection` external** (do not inline it into the bundle) so the runtime hook loads from `node_modules`. Most bundlers externalize `node_modules` for a Node target by default; - if yours inlines everything, mark `@sentry/server-utils` as external explicitly. + if yours inlines everything, mark `@sentry/server-runtime-injection` as external explicitly. 2. **Instrument at build time** with the Sentry bundler plugins (`@sentry/node/esbuild`, `@sentry/node/webpack`, `@sentry/node/vite`, `@sentry/node/rollup`), which inject the instrumentation into your bundled dependencies during the build. In this mode the runtime hook is not needed. -If you bundle `@sentry/server-utils` **and** don't use the build-time plugin, its internal code +If you bundle `@sentry/server-runtime-injection` **and** don't use the build-time plugin, its internal code transformer is stripped and runtime auto-instrumentation is disabled — `@sentry/node` warns at startup when it detects this. (When the build-time plugin is used, there is no warning, since instrumentation is already in place.) diff --git a/packages/node/package.json b/packages/node/package.json index c3045e02e893..5970c580d110 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -81,6 +81,7 @@ "@sentry/conventions": "^0.20.0", "@sentry/core": "10.67.0", "@sentry/opentelemetry": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 623a1ec3805c..d98b800c8abb 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -17,7 +17,7 @@ import { } from '@sentry/core'; import { isMainThread, parentPort } from 'node:worker_threads'; import { detectOrchestrionSetup, getErrorIntegrations, getTracingIntegrations } from '@sentry/server-utils'; -import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; +import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; import { consoleIntegration } from '../integrations/console'; diff --git a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts index 782b7a1c093d..fab55e968706 100644 --- a/packages/node/test/sdk/diagnosticsChannelInjection.test.ts +++ b/packages/node/test/sdk/diagnosticsChannelInjection.test.ts @@ -6,7 +6,7 @@ const { registerDiagnosticsChannelInjection, detectOrchestrionSetup } = vi.hoist detectOrchestrionSetup: vi.fn(), })); -vi.mock('@sentry/server-utils/orchestrion/register', () => ({ +vi.mock('@sentry/server-runtime-injection/register', () => ({ registerDiagnosticsChannelInjection, })); vi.mock('@sentry/server-utils', async importOriginal => { diff --git a/packages/nuxt/README.md b/packages/nuxt/README.md index 13fe27528588..695c6972400b 100644 --- a/packages/nuxt/README.md +++ b/packages/nuxt/README.md @@ -29,7 +29,7 @@ functionality related to Nuxt. ## Troubleshoot If your server-side auto-instrumentation stops recording spans after bundling (e.g. certain Nitro -presets), make sure `@sentry/server-utils` is kept **external** in the Nitro/server build rather than +presets), make sure `@sentry/server-runtime-injection` is kept **external** in the Nitro/server build rather than inlined — its runtime module hook must resolve from `node_modules`. `@sentry/node` logs a warning at startup when it detects it was bundled. diff --git a/packages/server-runtime-injection/README.md b/packages/server-runtime-injection/README.md new file mode 100644 index 000000000000..e7c09f0935f0 --- /dev/null +++ b/packages/server-runtime-injection/README.md @@ -0,0 +1,22 @@ +

+ + Sentry + +

+ +# Sentry Server Runtime Injection + +[![npm version](https://img.shields.io/npm/v/@sentry/server-runtime-injection.svg)](https://www.npmjs.com/package/@sentry/server-runtime-injection) + +This is an internal package for the Sentry JavaScript SDKs. It is not part of the public API contract +and may change at any time. + +It contains the **runtime** diagnostics-channel injection used by the server SDKs — the module hooks +that transform instrumented dependencies as they load at runtime (`register`, `hook`, `import-hook`), +together with the vendored code transformer they rely on. + +> **Important:** this package must be kept **external** (not bundled) when bundling a server. Its +> runtime hook loads a transformer that self-references its own on-disk `node_modules` location; +> bundling it strips the transformer and breaks that self-reference. When you bundle your server, +> either keep `@sentry/server-runtime-injection` external, or rely on the build-time instrumentation +> from the Sentry bundler plugins instead. diff --git a/packages/server-runtime-injection/package.json b/packages/server-runtime-injection/package.json new file mode 100644 index 000000000000..90cd06ef804c --- /dev/null +++ b/packages/server-runtime-injection/package.json @@ -0,0 +1,94 @@ +{ + "name": "@sentry/server-runtime-injection", + "version": "10.67.0", + "description": "Runtime diagnostics-channel injection hooks for the Sentry JavaScript SDKs. Must be kept external (not bundled) when bundling a server.", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/server-runtime-injection", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0 <23.0.0 || >=23.2.0" + }, + "files": [ + "/build" + ], + "main": "build/cjs/register.js", + "module": "build/esm/register.js", + "types": "build/types/register.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./build/types/register.d.ts", + "import": "./build/esm/register.js", + "require": "./build/cjs/register.js" + }, + "./register": { + "types": "./build/types/register.d.ts", + "import": "./build/esm/register.js", + "require": "./build/cjs/register.js" + }, + "./hook": { + "import": "./build/esm/hook.js" + }, + "./import-hook": { + "import": "./build/import-hook.mjs" + } + }, + "typesVersions": { + "*": { + "register": [ + "build/types/register.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/core": "10.67.0", + "@sentry/server-utils": "10.67.0" + }, + "devDependencies": { + "@apm-js-collab/code-transformer": "^0.18.1", + "@apm-js-collab/tracing-hooks": "^0.13.0", + "@types/node": "^18.19.1", + "meriyah": "^6.1.4" + }, + "scripts": { + "build": "run-p build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", + "build:watch": "run-p build:transpile:watch", + "build:dev:watch": "run-p build:transpile:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:tarball": "npm pack", + "clean": "rimraf build coverage sentry-server-runtime-injection-*.tgz", + "lint:fix": "oxlint . --fix --type-aware", + "lint": "oxlint . --type-aware", + "lint:types": "oxlint src --type-aware --type-check", + "lint:es-compatibility": "es-check es2020 ./build/cjs/*.js && es-check es2020 ./build/esm/*.js --module", + "test:unit": "vitest run", + "test": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false, + "nx": { + "targets": { + "build:transpile": { + "outputs": [ + "{projectRoot}/build/esm", + "{projectRoot}/build/cjs", + "{projectRoot}/build/npm/esm", + "{projectRoot}/build/npm/cjs", + "{projectRoot}/build/orchestrion", + "{projectRoot}/build/import-hook.mjs" + ] + } + } + } +} diff --git a/packages/server-runtime-injection/rollup.npm.config.mjs b/packages/server-runtime-injection/rollup.npm.config.mjs new file mode 100644 index 000000000000..8a121a88834f --- /dev/null +++ b/packages/server-runtime-injection/rollup.npm.config.mjs @@ -0,0 +1,116 @@ +import { builtinModules } from 'node:module'; +import commonjs from '@rollup/plugin-commonjs'; +import license from 'rollup-plugin-license'; +import { defineConfig } from 'rollup'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +// The orchestrion runtime dependency chain (`@apm-js-collab/tracing-hooks` → +// `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's +// build instead of installed as runtime dependencies. Everything in the chain is plain JS, and +// bundling removes two whole classes of downstream breakage: +// +// 1. `require(esm)`: the chain's only sync entry (`hook-sync.mjs`) is ESM-only, so an installed +// dependency forces our CJS build through Node's `require(esm)` bridge — unavailable on the AWS +// Lambda runtime (`--no-experimental-require-module`) and broken on `Module.register()` loader +// threads on Node 22.15–24.12 (`The resolveSync() method is not implemented`). Compiled into our +// own dual build, the CJS variant is genuine CJS. +// 2. Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is +// resolved differently by build-time tracers (`@vercel/nft`, nf3, Nitro externals) than by the +// runtime CJS loader, producing pruned server bundles that crash with `MODULE_NOT_FOUND` +// (https://github.com/vercel/nft/issues/603, https://github.com/nitrojs/nitro/issues/4456). +// Bundled, there is no runtime package resolution left to get wrong. +// +// `@sentry/*` deps (including `@sentry/server-utils`, from which `register.ts` imports +// `SENTRY_INSTRUMENTATIONS`) stay external — the base config keeps them out of the bundle, so they +// resolve from `node_modules` at runtime. +// +// `requireReturnsDefault: 'auto'`: node-resolve prefers a dependency's ESM build even for CJS +// `require()`s inside the vendored graph. Default-export-only ESM (e.g. esquery) must then resolve +// to the default itself, not a `{ default }` namespace — CJS callers use it as +// `require('esquery').parse(...)`. +// +// `strictRequires: false`: the default `'auto'` wraps conditionally-required modules (e.g. +// `debug`'s browser/node split) in lazy initializers exported as `__require` — an export name that +// downstream re-bundlers mishandle (Turbopack renames it, producing `.require is not a function` +// crashes in Next.js on Cloudflare). Hoisting is safe here: the vendored graph is closed (nothing +// optional/missing) and has no require cycles that depend on lazy evaluation. +const commonJSOptions = { transformMixedEsModules: true, requireReturnsDefault: 'auto', strictRequires: false }; +const commonJSPlugin = commonjs(commonJSOptions); + +// Always vendor `debug`'s Node build. Its default entry picks browser vs node at require time, +// which drags the browser build into this server-only bundle — and, hoisted by +// `strictRequires: false`, the browser build's storage detection probes `localStorage` at import +// time, which on Node >= 26 emits an ExperimentalWarning that pollutes stderr and console +// breadcrumbs in every user app. `order: 'pre'` because the base config's node-resolve plugin +// sorts ahead of package-specific plugins and would otherwise resolve `debug` first. +const debugNodeAlias = { + name: 'debug-node-alias', + resolveId: { + order: 'pre', + handler(source, importer) { + return source === 'debug' ? this.resolve('debug/src/node.js', importer, { skipSelf: true }) : null; + }, + }, +}; + +// Bundling files from the repo-root `node_modules` moves rollup's common source ancestor up to the +// repo root, so `preserveModules` names our own files `packages/server-runtime-injection/src/...` — +// strip that prefix to keep the `build/cjs/register.js` layout the `exports` map points at. And npm +// never packs `node_modules` directories, so the vendored dependencies must not be emitted under +// that name. +const sanitizedFileNames = info => + `${info.name.replace(/^packages\/server-runtime-injection\/src\//, '').replace(/node_modules/g, 'vendored')}.js`; + +// The vendored dependencies (see above) are third-party code redistributed inside this package's +// published `build/`, so their licenses require us to carry each one's copyright/permission notice +// (and, for Apache-2.0 deps like `@apm-js-collab/*`, the upstream NOTICE). Rollup strips per-file +// banners, so instead we aggregate them into a single `build/THIRD-PARTY-LICENSES.txt`. +const thirdPartyLicensePlugin = license({ + thirdParty: { + includePrivate: false, + output: { + file: 'build/THIRD-PARTY-LICENSES.txt', + }, + }, +}); + +const orchestrionRuntimeHooks = [ + // The side-effecting `--import` entry SDKs reference via a `--import` flag. We pass it through + // rollup only to copy it to `build/import-hook.mjs` at the path the package.json `exports` map + // expects; `external: /.*/` keeps every import (`@sentry/server-runtime-injection/register`) a + // runtime resolution against the installed package. + defineConfig({ + input: 'src/import-hook.mjs', + external: /.*/, + output: { format: 'esm', file: 'build/import-hook.mjs' }, + }), +]; + +export default [ + ...orchestrionRuntimeHooks, + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + // `register.ts` backs `./register` (the Node SDK `require`s it synchronously from + // `Sentry.init()`); `hook.mjs` backs `./hook` (the async `Module.register()` target, loaded on + // Node's ESM loader thread, which cannot resolve bare specifiers into the vendored chunks — but + // relative imports work, so it shares the ESM build's vendored chunks). `./hook` only maps its + // `import` condition, so the `build/cjs` copy is unused. + entrypoints: ['src/register.ts', 'src/hook.mjs'], + packageSpecificConfig: { + plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin], + output: { + exports: 'named', + preserveModules: true, + entryFileNames: sanitizedFileNames, + // The commonjs-converted vendored dependencies import Node builtins as default imports + // (`require('path')` → default import of `path`), and builtins have no `.default` in CJS — + // so builtins need `'default'` interop (the module itself is the default export). + interop: id => (id && (id.startsWith('node:') || builtinModules.includes(id)) ? 'default' : 'esModule'), + // The vendored dependencies import builtins unprefixed (`import … from 'tty'`), which Deno + // rejects and vite-node (Node 26) misresolves as a relative path. Emit them `node:`-prefixed. + paths: Object.fromEntries(builtinModules.map(m => [m, `node:${m}`])), + }, + }, + }), + ), +]; diff --git a/packages/server-utils/src/orchestrion/runtime/apm-js-collab-tracing-hooks.d.ts b/packages/server-runtime-injection/src/apm-js-collab-tracing-hooks.d.ts similarity index 100% rename from packages/server-utils/src/orchestrion/runtime/apm-js-collab-tracing-hooks.d.ts rename to packages/server-runtime-injection/src/apm-js-collab-tracing-hooks.d.ts diff --git a/packages/server-utils/src/orchestrion/runtime/hook.mjs b/packages/server-runtime-injection/src/hook.mjs similarity index 92% rename from packages/server-utils/src/orchestrion/runtime/hook.mjs rename to packages/server-runtime-injection/src/hook.mjs index 93460849cade..e0f187804615 100644 --- a/packages/server-utils/src/orchestrion/runtime/hook.mjs +++ b/packages/server-runtime-injection/src/hook.mjs @@ -6,5 +6,5 @@ // on-disk ES module graph — the loader thread cannot resolve bare specifiers into the dependency // graph this package bundles away, but it can follow relative imports. This shim is therefore an // entrypoint of the regular ESM build (sharing the vendored dependency chunks) and exposed via the -// `@sentry/server-utils/orchestrion/hook` subpath. +// `@sentry/server-runtime-injection/hook` subpath. export * from '@apm-js-collab/tracing-hooks/hook.mjs'; diff --git a/packages/server-utils/src/orchestrion/runtime/import-hook.mjs b/packages/server-runtime-injection/src/import-hook.mjs similarity index 88% rename from packages/server-utils/src/orchestrion/runtime/import-hook.mjs rename to packages/server-runtime-injection/src/import-hook.mjs index a99882e686e3..99f52d5a075d 100644 --- a/packages/server-utils/src/orchestrion/runtime/import-hook.mjs +++ b/packages/server-runtime-injection/src/import-hook.mjs @@ -8,11 +8,11 @@ // and the `init()` path can never drift apart. This file is just the // side-effecting wrapper that invokes it. // -// This file is shipped as-is to `build/orchestrion/import-hook.mjs`. Keep it a +// This file is shipped as-is to `build/import-hook.mjs`. Keep it a // single self-contained `.mjs` file with no relative-path imports — `--import` // resolves it (and the bare specifier below) via Node's module resolution // against the installed package. -import { registerDiagnosticsChannelInjection } from '@sentry/server-utils/orchestrion/register'; +import { registerDiagnosticsChannelInjection } from '@sentry/server-runtime-injection/register'; registerDiagnosticsChannelInjection(); diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-runtime-injection/src/register.ts similarity index 89% rename from packages/server-utils/src/orchestrion/runtime/register.ts rename to packages/server-runtime-injection/src/register.ts index 340bdaf066d4..4a5bd458502a 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-runtime-injection/src/register.ts @@ -2,7 +2,7 @@ import { consoleSandbox, debug, getClient, GLOBAL_OBJ, parseSemver } from '@sent import * as Module from 'node:module'; import { pathToFileURL } from 'node:url'; import { create } from '@apm-js-collab/code-transformer'; -import { SENTRY_INSTRUMENTATIONS } from '../config'; +import { SENTRY_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config'; import type { register } from 'node:module'; import ModulePatch from '@apm-js-collab/tracing-hooks'; import { initialize, load, resolve, createDiagnosticsPort } from '@apm-js-collab/tracing-hooks/hook-sync.mjs'; @@ -32,8 +32,8 @@ function hasStableSyncModuleHooks(isDeno: boolean): boolean { * package) survived downstream bundling. * * This package ships the transformer inline and is meant to run from `node_modules` (external). When - * an app bundler instead inlines `@sentry/server-utils` and tree-shakes it, those vendored deps are - * stripped to empty objects, so `parse`/`generate` become `undefined` and the FIRST module the hook + * an app bundler instead inlines `@sentry/server-runtime-injection` and tree-shakes it, those vendored + * deps are stripped to empty objects, so `parse`/`generate` become `undefined` and the FIRST module the hook * tries to transform throws `TypeError: parse is not a function` — deep in the loader, once per * module, only visible with `debug: true`. Running one throwaway in-memory transform up front turns * that into a single, actionable, always-on warning (see `warnRuntimeUnavailable`). A healthy build @@ -105,14 +105,14 @@ export function registerDiagnosticsChannelInjection(): void { // surface an always-on, actionable warning. if (marker.bundler instanceof Set) { debug.log( - 'Runtime diagnostics-channel injection is disabled because `@sentry/server-utils` was bundled; ' + - 'build-time instrumentation is active.', + 'Runtime diagnostics-channel injection is disabled because `@sentry/server-runtime-injection` was ' + + 'bundled; build-time instrumentation is active.', ); } else { warnRuntimeUnavailable( - '`@sentry/server-utils` was bundled into your application, so diagnostics-channel ' + - 'auto-instrumentation is disabled. Keep `@sentry/server-utils` external in your server bundle, ' + - 'or use the Sentry bundler plugin for build-time instrumentation.', + '`@sentry/server-runtime-injection` was bundled into your application, so diagnostics-channel ' + + 'auto-instrumentation is disabled. Keep `@sentry/server-runtime-injection` external in your server ' + + 'bundle, or use the Sentry bundler plugin for build-time instrumentation.', ); } return; @@ -163,10 +163,11 @@ export function registerDiagnosticsChannelInjection(): void { parentURL = import.meta.url; /*! rollup-include-esm-only-end */ - // Our own bundled copy of the tracing-hooks async hooks (see - // `src/orchestrion/runtime/hook.mjs`) — the dependency itself is bundled into this package's - // build and no longer resolvable as a bare specifier at runtime. - mod.register('@sentry/server-utils/orchestrion/hook', { + // Our own bundled copy of the tracing-hooks async hooks (see `src/hook.mjs`) — the dependency + // itself is bundled into this package's build and no longer resolvable as a bare specifier at + // runtime. This self-referential specifier only resolves while this package lives at its real + // `node_modules` location, which is why it must stay external (never bundled into an app). + mod.register('@sentry/server-runtime-injection/hook', { parentURL, data: { instrumentations: SENTRY_INSTRUMENTATIONS, diagnosticsPort }, transferList: [diagnosticsPort], diff --git a/packages/server-utils/test/orchestrion/register.test.ts b/packages/server-runtime-injection/test/register.test.ts similarity index 97% rename from packages/server-utils/test/orchestrion/register.test.ts rename to packages/server-runtime-injection/test/register.test.ts index b21e0227d9ae..af34afab7712 100644 --- a/packages/server-utils/test/orchestrion/register.test.ts +++ b/packages/server-runtime-injection/test/register.test.ts @@ -17,7 +17,7 @@ vi.mock('@sentry/core', async importOriginal => { }); import { GLOBAL_OBJ } from '@sentry/core'; -import { registerDiagnosticsChannelInjection } from '../../src/orchestrion/runtime/register'; +import { registerDiagnosticsChannelInjection } from '../src/register'; describe('registerDiagnosticsChannelInjection - bundled/tree-shaken detection', () => { let warnSpy: ReturnType; diff --git a/packages/server-runtime-injection/tsconfig.json b/packages/server-runtime-injection/tsconfig.json new file mode 100644 index 000000000000..6af250d0358c --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": {}, + // `hook.mjs` and `import-hook.mjs` are hand-written `.mjs` files that self-reference this package's + // own subpaths. If tsc picks them up it follows those subpath exports back to emitted `.d.ts` + // files and collides with what it wants to emit. Excluding them keeps tsc focused on the `.ts` + // sources — rollup copies the `.mjs` files through to `build/` unchanged. + "exclude": ["src/**/*.mjs", "src/**/*.cjs"] +} diff --git a/packages/server-runtime-injection/tsconfig.test.json b/packages/server-runtime-injection/tsconfig.test.json new file mode 100644 index 000000000000..851f2a16b733 --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.test.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node", "vitest"] + } +} diff --git a/packages/server-runtime-injection/tsconfig.types.json b/packages/server-runtime-injection/tsconfig.types.json new file mode 100644 index 000000000000..ab12a03a64f2 --- /dev/null +++ b/packages/server-runtime-injection/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types", + "rootDir": "src" + } +} diff --git a/packages/server-runtime-injection/vite.config.ts b/packages/server-runtime-injection/vite.config.ts new file mode 100644 index 000000000000..841ff483d7c4 --- /dev/null +++ b/packages/server-runtime-injection/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + }, +}); diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 51360b990abf..20ac27e66f33 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -32,11 +32,6 @@ "import": "./build/esm/orchestrion/config/index.js", "require": "./build/cjs/orchestrion/config/index.js" }, - "./orchestrion/register": { - "types": "./build/types/orchestrion/runtime/register.d.ts", - "import": "./build/esm/orchestrion/runtime/register.js", - "require": "./build/cjs/orchestrion/runtime/register.js" - }, "./orchestrion/vite": { "types": "./build/types/orchestrion/bundler/vite.d.ts", "import": "./build/esm/orchestrion/bundler/vite.js", @@ -61,11 +56,10 @@ "import": "./build/esm/orchestrion/bundler/esbuild.js", "require": "./build/cjs/orchestrion/bundler/esbuild.js" }, - "./orchestrion/import-hook": { - "import": "./build/orchestrion/import-hook.mjs" - }, - "./orchestrion/hook": { - "import": "./build/esm/orchestrion/runtime/hook.js" + "./orchestrion/bundler-transforms": { + "types": "./build/types/orchestrion/bundler/moduleInjectedTransform.d.ts", + "import": "./build/esm/orchestrion/bundler/moduleInjectedTransform.js", + "require": "./build/cjs/orchestrion/bundler/moduleInjectedTransform.js" } }, "typesVersions": { @@ -73,9 +67,6 @@ "orchestrion/config": [ "build/types/orchestrion/config/index.d.ts" ], - "orchestrion/register": [ - "build/types/orchestrion/runtime/register.d.ts" - ], "orchestrion/vite": [ "build/types/orchestrion/bundler/vite.d.ts" ], @@ -87,6 +78,9 @@ ], "orchestrion/esbuild": [ "build/types/orchestrion/bundler/esbuild.d.ts" + ], + "orchestrion/bundler-transforms": [ + "build/types/orchestrion/bundler/moduleInjectedTransform.d.ts" ] } }, @@ -101,7 +95,6 @@ "devDependencies": { "@apm-js-collab/code-transformer": "^0.18.1", "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", - "@apm-js-collab/tracing-hooks": "^0.13.0", "@types/node": "^18.19.1", "meriyah": "^6.1.4", "vite": "^6.4.3" @@ -136,8 +129,7 @@ "{projectRoot}/build/esm", "{projectRoot}/build/cjs", "{projectRoot}/build/npm/esm", - "{projectRoot}/build/npm/cjs", - "{projectRoot}/build/orchestrion" + "{projectRoot}/build/npm/cjs" ] } } diff --git a/packages/server-utils/rollup.npm.config.mjs b/packages/server-utils/rollup.npm.config.mjs index 6b7c30b5a292..1a64c3879e17 100644 --- a/packages/server-utils/rollup.npm.config.mjs +++ b/packages/server-utils/rollup.npm.config.mjs @@ -1,20 +1,15 @@ import { builtinModules } from 'node:module'; import commonjs from '@rollup/plugin-commonjs'; import license from 'rollup-plugin-license'; -import { defineConfig } from 'rollup'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -// The orchestrion runtime dependency chain (`@apm-js-collab/tracing-hooks` → -// `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's -// build instead of installed as runtime dependencies. Everything in the chain is plain JS, and -// bundling removes two whole classes of downstream breakage: +// The orchestrion build-time bundler-plugin chain (`@apm-js-collab/code-transformer-bundler-plugins` +// → `@apm-js-collab/code-transformer` → meriyah/esquery/astring/…) is bundled into this package's +// build instead of installed as runtime dependencies. (The runtime injection chain lives in +// `@sentry/server-runtime-injection`.) Everything here is plain JS, and bundling removes a class of +// downstream breakage: // -// 1. `require(esm)`: the chain's only sync entry (`hook-sync.mjs`) is ESM-only, so an installed -// dependency forces our CJS build through Node's `require(esm)` bridge — unavailable on the AWS -// Lambda runtime (`--no-experimental-require-module`) and broken on `Module.register()` loader -// threads on Node 22.15–24.12 (`The resolveSync() method is not implemented`). Compiled into our -// own dual build, the CJS variant is genuine CJS. -// 2. Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is +// Tracer/runtime exports-map mismatches: meriyah 6.1's `module-sync`-first exports map is // resolved differently by build-time tracers (`@vercel/nft`, nf3, Nitro externals) than by the // runtime CJS loader, producing pruned server bundles that crash with `MODULE_NOT_FOUND` // (https://github.com/vercel/nft/issues/603, https://github.com/nitrojs/nitro/issues/4456). @@ -79,21 +74,7 @@ const thirdPartyLicensePlugin = license({ }, }); -const orchestrionRuntimeHooks = [ - // EXPERIMENTAL — orchestrion.js runtime hook. A hand-written `.mjs` shim that SDKs reference via - // a `--import .../orchestrion/import-hook` flag. We pass it through rollup only to copy it into - // `build/orchestrion/` at the path the package.json `exports` map expects; `external: /.*/` keeps - // every import (e.g. `@sentry/server-utils/orchestrion/config`) as a runtime resolution against - // the installed package. - defineConfig({ - input: 'src/orchestrion/runtime/import-hook.mjs', - external: /.*/, - output: { format: 'esm', file: 'build/orchestrion/import-hook.mjs' }, - }), -]; - export default [ - ...orchestrionRuntimeHooks, ...makeNPMConfigVariants( makeBaseNPMConfig({ // `src/orchestrion/config/index.ts` and the `src/orchestrion/bundler/*.ts` @@ -101,21 +82,14 @@ export default [ // `.../orchestrion/vite`, etc.) — none are reachable from `src/index.ts`, so // we list them as separate entrypoints to guarantee they end up in build/esm // and build/cjs. + // + // The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the vendored + // transformer chain) lives in `@sentry/server-runtime-injection` — it must stay external when + // apps bundle, so it is a separate package rather than a subpath here. entrypoints: [ 'src/index.ts', 'src/index.no-diagnostic-channels.ts', 'src/orchestrion/config/index.ts', - // `src/orchestrion/runtime/register.ts` backs the `./orchestrion/register` - // subpath export; the Node SDK `require`s it synchronously from - // `Sentry.init()` to install the channel-injection hooks. - 'src/orchestrion/runtime/register.ts', - // The async module hooks passed to `Module.register()`. They load on Node's ESM loader - // thread, which cannot resolve bare specifiers into our bundled dependency graph — but - // relative imports of on-disk files work, and `build/esm` is a `"type": "module"` scope, so - // this entrypoint shares the vendored chunks with the rest of the build. The `./orchestrion/ - // hook` export only maps its `import` condition (nothing ever `require()`s it), so the copy - // in `build/cjs` is unused. - 'src/orchestrion/runtime/hook.mjs', 'src/orchestrion/bundler/vite.ts', 'src/orchestrion/bundler/rollup.ts', 'src/orchestrion/bundler/webpack.ts', diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index d105b967a72a..c2e8415448e8 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -34,11 +34,6 @@ import { vercelAiConfig } from './vercel-ai'; // Kept sorted alphabetically by module so concurrent additions insert at different // points rather than all appending to the end (fewer merge conflicts). -// Re-exported here for bundler integrations that compose the upstream -// code-transformer plugin themselves instead of using one of our wrappers -// (`@sentry/bun`'s plugin uses the upstream `/bun` entry directly). -export { moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER } from '../bundler/moduleInjectedTransform'; - /** * The orchestrion code-transform configs. Every instrumentable library is here * so the transform is all-or-nothing: whenever orchestrion is enabled, all of diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts index 1c1cd1c32977..86ccfb1a42c9 100644 --- a/packages/server-utils/test/orchestrion/bundler.test.ts +++ b/packages/server-utils/test/orchestrion/bundler.test.ts @@ -285,13 +285,8 @@ describe('buildTimeInstrumentation: false', () => { describe('resolveOrchestrionRuntimeRequest', () => { it.each([ // Self-references — resolve through this package's own exports map to the CJS build. - '@sentry/server-utils/orchestrion/register', '@sentry/server-utils/orchestrion/config', - // Dependencies of this package, including subpaths only reachable from its location. - '@apm-js-collab/tracing-hooks', - '@apm-js-collab/tracing-hooks/hook.mjs', - '@apm-js-collab/tracing-hooks/hook-sync.mjs', - '@apm-js-collab/tracing-hooks/lib/diagnostics.js', + // Dependencies of this package, resolvable only from its location. '@apm-js-collab/code-transformer', ])('resolves %s to an existing absolute path', request => { const resolved = resolveOrchestrionRuntimeRequest(request); @@ -302,7 +297,7 @@ describe('resolveOrchestrionRuntimeRequest', () => { }); it('resolves self-references with require conditions, so the paths are loadable via require()', () => { - expect(resolveOrchestrionRuntimeRequest('@sentry/server-utils/orchestrion/register')).toMatch(/[/\\]cjs[/\\]/); + expect(resolveOrchestrionRuntimeRequest('@sentry/server-utils/orchestrion/config')).toMatch(/[/\\]cjs[/\\]/); }); it('returns undefined for unresolvable requests', () => { diff --git a/packages/server-utils/tsconfig.json b/packages/server-utils/tsconfig.json index 72d1dd3759a1..b0eb9ecb6476 100644 --- a/packages/server-utils/tsconfig.json +++ b/packages/server-utils/tsconfig.json @@ -3,12 +3,5 @@ "include": ["src/**/*"], - "compilerOptions": {}, - // The orchestrion runtime hook is a hand-written `.mjs` file that self-references - // `@sentry/server-utils/orchestrion/config`. If tsc picks it up, it - // follows that subpath export back to `build/types/orchestrion/config/index.d.ts`, - // treats the .d.ts as an input, and then collides with the .d.ts it wants to - // emit from `src/orchestrion/config/index.ts`. Excluding it keeps tsc focused on the - // .ts sources — rollup copies the file through to `build/orchestrion/` unchanged. - "exclude": ["src/orchestrion/runtime/**/*.mjs", "src/orchestrion/runtime/**/*.cjs"] + "compilerOptions": {} } diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 56840786c1bb..14ac56214323 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -57,6 +57,7 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "@sentry/bundler-plugins": "10.67.0" }, From 8f2f48b6ad845f82997378f63f8fba59d409e169 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:28:49 +0200 Subject: [PATCH 2/4] refs and fixes --- packages/bun/src/plugin.ts | 123 +----------------- packages/server-utils/package.json | 12 +- packages/server-utils/rollup.npm.config.mjs | 5 +- .../src/orchestrion/bundler/bun.ts | 88 +++++++++++++ 4 files changed, 97 insertions(+), 131 deletions(-) create mode 100644 packages/server-utils/src/orchestrion/bundler/bun.ts diff --git a/packages/bun/src/plugin.ts b/packages/bun/src/plugin.ts index 956ee611ac8b..83c42f3126cd 100644 --- a/packages/bun/src/plugin.ts +++ b/packages/bun/src/plugin.ts @@ -11,127 +11,8 @@ * }); * ``` * - * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is intentionally not - * offered: a module returned by a runtime `onLoad` plugin in Bun loses its - * CommonJS named exports. - * - * When https://github.com/oven-sh/bun/pull/31770 lands, we can revisit. - * - * Until then, Bun apps must bundle to get build-time instrumentation. In dev - * (ie, `bun run`) there is simply no instrumentation, which is clearer than - * partial/inconsistent coverage. - * - * Shipped as both ESM and CJS (via the `@sentry/bun/plugin` subpath) so a user's - * `bun build` script can be authored in either module system. It's a plain - * library import here (not a `--import`/`--preload` hook), so CJS is fine; Bun - * resolves the underlying ESM-only transformer in either module system. + * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is currently not supported. * * @module */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type UnknownPlugin = any; - -// `@apm-js-collab/code-transformer-bundler-plugins/bun` is published ESM-only -// (no `require` arm, unlike its `/vite` entry). The ESM build imports it; the -// CJS build requires it. Bun resolves correctly for ESM modules in either -// module system. -import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; -import { - moduleInjectedTransforms, - ORCHESTRION_BUNDLER_MARKER_BANNER, -} from '@sentry/server-utils/orchestrion/bundler-transforms'; -import { - INSTRUMENTED_MODULE_NAMES, - SENTRY_INSTRUMENTATIONS, - withoutInstrumentedExternals, -} from '@sentry/server-utils/orchestrion/config'; - -// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead -// of depending on `bun-types`, which would pull Bun's globals. -interface BunPluginBuilder { - config?: { banner?: string; external?: string[]; packages?: 'bundle' | 'external' }; -} - -/** - * Returns the Sentry code-transform plugin for Bun's bundler, configured - * with the central `SENTRY_INSTRUMENTATIONS`. The plugin injects - * `diagnostics_channel.tracingChannel` calls into the instrumented libraries as - * `bun build` bundles them — plus, via the module-injected transform, the - * snippet that records each module on `globalThis.__SENTRY_ORCHESTRION__` when - * it is evaluated — and injects the marker banner so `bundler` is set (to an - * empty `Set`) from boot, which is what gates the SDK's channel-integration - * setup at `init()`. - * - * Pass the result to `Bun.build({ plugins: [...] })`. - * - * @example - * ```ts - * import { sentryBunPlugin } from '@sentry/bun/plugin'; - * await Bun.build({ entrypoints: ['./app.ts'], plugins: [sentryBunPlugin()] }); - * ``` - */ -export function sentryBunPlugin(): UnknownPlugin { - // Typed upstream as an esbuild `Plugin`, but Bun passes its own - // `PluginBuilder` (which has the `onLoad` the transform uses) to `setup`. - // Cast to the Bun-compatible shape so we can forward Bun's builder to its - // `setup`. - const transformer = codeTransformer({ - instrumentations: SENTRY_INSTRUMENTATIONS, - customTransforms: moduleInjectedTransforms(), - }) as unknown as { - setup: (build: BunPluginBuilder) => void; - }; - - return { - name: 'sentry-orchestrion', - setup(build: BunPluginBuilder): void { - // Inject the marker banner via Bun's native `banner` config (unlike the - // upstream `injectDiagnostics` path, it needs no `outdir`). `config` is - // the `Bun.build` config and is present when this plugin is passed to - // `Bun.build({ plugins: [...] })`. - if (build.config) { - const existing = build.config.banner ?? ''; - build.config.banner = existing - ? `${existing}\n${ORCHESTRION_BUNDLER_MARKER_BANNER}` - : ORCHESTRION_BUNDLER_MARKER_BANNER; - - // Force-bundle every instrumented package. An externalized dependency - // is resolved from `node_modules` at runtime and never passes throug - // the transform's `onLoad`, so its diagnostics_channel calls would - // be silently never injected. Bun has no runtime fallback here, so - // bundling is the only injection path. - build.config.external = withoutInstrumentedExternals(build.config.external); - - // A blanket externalization strategy like `packages: 'external'` or - // `'*'` in `external` externalizes instrumented packages too, and - // `withoutInstrumentedExternals` only strips exact names/subpaths (not - // these), so those packages ship un-transformed with no runtime - // fallback. Forcing them back in via `onResolve` is not an option: Bun - // ignores `{ external: false }` against a blanket strategy, and - // returning a resolved `path` corrupts the package's ESM/CJS interop. - // So warn instead. This runs in the user's build script, where the - // Sentry debug logger isn't enabled, and `console` is the thing to use. - const blanketExternal = - build.config.packages === 'external' - ? "packages: 'external'" - : build.config.external?.includes('*') - ? "'*' in external" - : undefined; - if (blanketExternal) { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] This Bun build externalizes all dependencies (${blanketExternal}), so Sentry ` + - 'cannot instrument bundled libraries. Instrumentation will be missing for any of ' + - `these packages your app uses: ${INSTRUMENTED_MODULE_NAMES.join(', ')}. To instrument them, ` + - 'externalize only the specific packages you need external instead of all of them.', - ); - } - } - - // Delegate to the upstream code-transformer, which registers the `onLoad` - // hook that does the actual channel injection. - transformer.setup(build); - }, - }; -} +export { sentryOrchestrionPlugin as sentryBunPlugin } from '@sentry/server-utils/orchestrion/bun'; diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json index 20ac27e66f33..83f0137541e6 100644 --- a/packages/server-utils/package.json +++ b/packages/server-utils/package.json @@ -56,10 +56,10 @@ "import": "./build/esm/orchestrion/bundler/esbuild.js", "require": "./build/cjs/orchestrion/bundler/esbuild.js" }, - "./orchestrion/bundler-transforms": { - "types": "./build/types/orchestrion/bundler/moduleInjectedTransform.d.ts", - "import": "./build/esm/orchestrion/bundler/moduleInjectedTransform.js", - "require": "./build/cjs/orchestrion/bundler/moduleInjectedTransform.js" + "./orchestrion/bun": { + "types": "./build/types/orchestrion/bundler/bun.d.ts", + "import": "./build/esm/orchestrion/bundler/bun.js", + "require": "./build/cjs/orchestrion/bundler/bun.js" } }, "typesVersions": { @@ -79,8 +79,8 @@ "orchestrion/esbuild": [ "build/types/orchestrion/bundler/esbuild.d.ts" ], - "orchestrion/bundler-transforms": [ - "build/types/orchestrion/bundler/moduleInjectedTransform.d.ts" + "orchestrion/bun": [ + "build/types/orchestrion/bundler/bun.d.ts" ] } }, diff --git a/packages/server-utils/rollup.npm.config.mjs b/packages/server-utils/rollup.npm.config.mjs index 1a64c3879e17..675a81424abe 100644 --- a/packages/server-utils/rollup.npm.config.mjs +++ b/packages/server-utils/rollup.npm.config.mjs @@ -82,10 +82,6 @@ export default [ // `.../orchestrion/vite`, etc.) — none are reachable from `src/index.ts`, so // we list them as separate entrypoints to guarantee they end up in build/esm // and build/cjs. - // - // The runtime diagnostics-channel injection (`register`/`hook`/`import-hook` + the vendored - // transformer chain) lives in `@sentry/server-runtime-injection` — it must stay external when - // apps bundle, so it is a separate package rather than a subpath here. entrypoints: [ 'src/index.ts', 'src/index.no-diagnostic-channels.ts', @@ -95,6 +91,7 @@ export default [ 'src/orchestrion/bundler/webpack.ts', 'src/orchestrion/bundler/webpack-loader.ts', 'src/orchestrion/bundler/esbuild.ts', + 'src/orchestrion/bundler/bun.ts', ], packageSpecificConfig: { plugins: [debugNodeAlias, commonJSPlugin, thirdPartyLicensePlugin], diff --git a/packages/server-utils/src/orchestrion/bundler/bun.ts b/packages/server-utils/src/orchestrion/bundler/bun.ts new file mode 100644 index 000000000000..8fb84b4ff201 --- /dev/null +++ b/packages/server-utils/src/orchestrion/bundler/bun.ts @@ -0,0 +1,88 @@ +import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun'; +import { INSTRUMENTED_MODULE_NAMES, SENTRY_INSTRUMENTATIONS, withoutInstrumentedExternals } from '../config'; +import { moduleInjectedTransforms, ORCHESTRION_BUNDLER_MARKER_BANNER } from './moduleInjectedTransform'; + +// oxlint-disable-next-line typescript/no-explicit-any +type UnknownPlugin = any; + +// Minimal shape of Bun's `PluginBuilder` that we touch. Typed locally instead +// of depending on `bun-types`, which would pull Bun's globals into this build. +interface BunPluginBuilder { + config?: { banner?: string; external?: string[]; packages?: 'bundle' | 'external' }; +} + +/** + * Sentry orchestrion code-transform plugin for Bun's bundler (`bun build`), exposed to users via the + * `@sentry/bun/plugin` subpath (which re-exports this as `sentryBunPlugin`). + * + * This is BUILD-ONLY. Runtime instrumentation (`bun run`) is intentionally not offered: a module + * returned by a runtime `onLoad` plugin in Bun loses its CommonJS named exports. When + * https://github.com/oven-sh/bun/pull/31770 lands, we can revisit. Until then, Bun apps must bundle + * to get build-time instrumentation; in dev (`bun run`) there is simply no instrumentation, which is + * clearer than partial/inconsistent coverage. + * + * The plugin injects `diagnostics_channel.tracingChannel` calls into the instrumented libraries as + * `bun build` bundles them — plus, via the module-injected transform, the snippet that records each + * module on `globalThis.__SENTRY_ORCHESTRION__` when it is evaluated — and injects the marker banner + * so `bundler` is set (to an empty `Set`) from boot, which gates the SDK's channel-integration setup + * at `init()`. + */ +export function sentryOrchestrionPlugin(): UnknownPlugin { + // Typed upstream as an esbuild `Plugin`, but Bun passes its own `PluginBuilder` (which has the + // `onLoad` the transform uses) to `setup`. Cast to the Bun-compatible shape so we can forward + // Bun's builder to its `setup`. + const transformer = codeTransformer({ + instrumentations: SENTRY_INSTRUMENTATIONS, + customTransforms: moduleInjectedTransforms(), + }) as unknown as { + setup: (build: BunPluginBuilder) => void; + }; + + return { + name: 'sentry-orchestrion', + setup(build: BunPluginBuilder): void { + // Inject the marker banner via Bun's native `banner` config (unlike the upstream + // `injectDiagnostics` path, it needs no `outdir`). `config` is the `Bun.build` config and is + // present when this plugin is passed to `Bun.build({ plugins: [...] })`. + if (build.config) { + const existing = build.config.banner ?? ''; + build.config.banner = existing + ? `${existing}\n${ORCHESTRION_BUNDLER_MARKER_BANNER}` + : ORCHESTRION_BUNDLER_MARKER_BANNER; + + // Force-bundle every instrumented package. An externalized dependency is resolved from + // `node_modules` at runtime and never passes through the transform's `onLoad`, so its + // diagnostics_channel calls would be silently never injected. Bun has no runtime fallback + // here, so bundling is the only injection path. + build.config.external = withoutInstrumentedExternals(build.config.external); + + // A blanket externalization strategy like `packages: 'external'` or `'*'` in `external` + // externalizes instrumented packages too, and `withoutInstrumentedExternals` only strips + // exact names/subpaths (not these), so those packages ship un-transformed with no runtime + // fallback. Forcing them back in via `onResolve` is not an option: Bun ignores + // `{ external: false }` against a blanket strategy, and returning a resolved `path` corrupts + // the package's ESM/CJS interop. So warn instead. This runs in the user's build script, + // where the Sentry debug logger isn't enabled, and `console` is the thing to use. + const blanketExternal = + build.config.packages === 'external' + ? "packages: 'external'" + : build.config.external?.includes('*') + ? "'*' in external" + : undefined; + if (blanketExternal) { + // oxlint-disable-next-line no-console + console.warn( + `[Sentry] This Bun build externalizes all dependencies (${blanketExternal}), so Sentry ` + + 'cannot instrument bundled libraries. Instrumentation will be missing for any of ' + + `these packages your app uses: ${INSTRUMENTED_MODULE_NAMES.join(', ')}. To instrument them, ` + + 'externalize only the specific packages you need external instead of all of them.', + ); + } + } + + // Delegate to the upstream code-transformer, which registers the `onLoad` hook that does the + // actual channel injection. + transformer.setup(build); + }, + }; +} From fe6c45be9958916e46db18cae7193967dc61c0b0 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:35:22 +0200 Subject: [PATCH 3/4] fix remix --- packages/remix/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/remix/package.json b/packages/remix/package.json index 22dc3d368c58..ba51cb8e10fe 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -58,6 +58,7 @@ "@sentry/core": "10.67.0", "@sentry/node": "10.67.0", "@sentry/react": "10.67.0", + "@sentry/server-runtime-injection": "10.67.0", "@sentry/server-utils": "10.67.0", "yargs": "^17.6.0" }, From 2a2a5e3756a6f4d814b3c74450abb680dc06b9ba Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 27 Aug 2026 15:42:42 +0200 Subject: [PATCH 4/4] fix(bun): Drop the now-unused @apm-js-collab/code-transformer-bundler-plugins dependency `@sentry/bun/plugin` now only re-exports from `@sentry/server-utils/orchestrion/bun` (which vendors the transformer), so the direct import is gone. Remove the leftover runtime dependency so installs don't pull an unused package. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/bun/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bun/package.json b/packages/bun/package.json index 49cf849095b0..e8cc53f11073 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -42,7 +42,6 @@ "access": "public" }, "dependencies": { - "@apm-js-collab/code-transformer-bundler-plugins": "^0.7.4", "@sentry/core": "10.67.0", "@sentry/conventions": "^0.20.0", "@sentry/node": "10.67.0",