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 fb99bd6143fe..b598841922c5 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/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",
diff --git a/packages/bun/src/plugin.ts b/packages/bun/src/plugin.ts
index 7c0e3abf3637..83c42f3126cd 100644
--- a/packages/bun/src/plugin.ts
+++ b/packages/bun/src/plugin.ts
@@ -11,125 +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 {
- INSTRUMENTED_MODULE_NAMES,
- moduleInjectedTransforms,
- ORCHESTRION_BUNDLER_MARKER_BANNER,
- 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/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts
index 9b56621fd389..e26e9db8933d 100644
--- a/packages/core/src/utils/worldwide.ts
+++ b/packages/core/src/utils/worldwide.ts
@@ -79,9 +79,10 @@ export type InternalGlobal = {
integrations?: Map Integration>;
/**
* Set once `registerDiagnosticsChannelInjection()` has run but could not
- * install the runtime module hooks — the Node runtime lacks the required
- * module-hook API, or registration threw. Dedupes the one-time warning and
- * short-circuits repeat calls.
+ * install the runtime module hooks — most commonly because
+ * `@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.
*/
runtimeUnavailable?: boolean;
};
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..fcb82513f330 100644
--- a/packages/nextjs/scripts/buildRollup.ts
+++ b/packages/nextjs/scripts/buildRollup.ts
@@ -24,28 +24,32 @@ 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)) {
+// Clear stale forwarders first: without this, a subpath removed from the exports map would leave its
+// generated forwarder behind on an incremental build.
+fs.rmSync(orchestrionRuntimeBuildDir, { recursive: true, force: true });
+
+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 49ee5dbd9e05..a71d049b777f 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 the
first time an instrumented dependency loads uninstrumented, so a build-time-instrumented app never
sees the warning.
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 c4a2eef6eae6..2c90dcc21d93 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, as its runtime module hook must resolve from `node_modules`. `@sentry/node` logs a warning
the first time an instrumented dependency loads uninstrumented.
diff --git a/packages/remix/package.json b/packages/remix/package.json
index 1c40dd2b0be6..e1b4fc5d3f4f 100644
--- a/packages/remix/package.json
+++ b/packages/remix/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": "^0.44.0",
"yargs": "^17.6.0"
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 Server Runtime Injection
+
+[](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..15e9f215eab4
--- /dev/null
+++ b/packages/server-runtime-injection/package.json
@@ -0,0 +1,93 @@
+{
+ "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": [
+ "**/import-hook.mjs"
+ ],
+ "nx": {
+ "targets": {
+ "build:transpile": {
+ "outputs": [
+ "{projectRoot}/build/esm",
+ "{projectRoot}/build/cjs",
+ "{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 91%
rename from packages/server-utils/src/orchestrion/runtime/register.ts
rename to packages/server-runtime-injection/src/register.ts
index b446accba64f..288bc571a6f5 100644
--- a/packages/server-utils/src/orchestrion/runtime/register.ts
+++ b/packages/server-runtime-injection/src/register.ts
@@ -1,7 +1,7 @@
import { consoleSandbox, debug, getClient, GLOBAL_OBJ, parseSemver } from '@sentry/core';
import * as Module from 'node:module';
import { pathToFileURL } from 'node:url';
-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';
@@ -45,7 +45,7 @@ let warnedTransformerUnavailable = false;
* Warn that the vendored code transformer could not run, so `moduleName` loaded uninstrumented.
*
* This package ships the transformer (meriyah/astring/source-map) inline and is meant to run from
- * `node_modules`. A bundler that inlines and tree-shakes `@sentry/server-utils` strips it, so every
+ * `node_modules`. A bundler that inlines and tree-shakes `@sentry/server-runtime-injection` strips it, so every
* transform throws `TypeError: parse is not a function` — swallowed inside the loader, once per
* module, visible only with `debug: true`.
*
@@ -62,8 +62,8 @@ function warnTransformerUnavailable(moduleName: string): void {
warnedTransformerUnavailable = true;
warnRuntimeUnavailable(
- `\`@sentry/server-utils\` was bundled into your application, so ${moduleName} and any other ` +
- 'instrumented dependency load uninstrumented. Keep `@sentry/server-utils` external in your ' +
+ `\`@sentry/server-runtime-injection\` was bundled into your application, so ${moduleName} and any other ` +
+ 'instrumented dependency load uninstrumented. Keep `@sentry/server-runtime-injection` external in your ' +
'server bundle, or use the Sentry bundler plugin for build-time instrumentation.',
);
}
@@ -138,10 +138,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 98%
rename from packages/server-utils/test/orchestrion/register.test.ts
rename to packages/server-runtime-injection/test/register.test.ts
index 0b44f19dd73c..2170f3b4de8d 100644
--- a/packages/server-utils/test/orchestrion/register.test.ts
+++ b/packages/server-runtime-injection/test/register.test.ts
@@ -36,7 +36,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';
type DiagnosticsCallback = (event: { moduleName: string; error?: unknown }) => void;
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..b8b47344fecb
--- /dev/null
+++ b/packages/server-runtime-injection/vite.config.ts
@@ -0,0 +1,22 @@
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+import baseConfig from '../../vite/vite.config';
+
+// `register.ts` imports `SENTRY_INSTRUMENTATIONS` from `@sentry/server-utils/orchestrion/config`,
+// whose exports map resolves to `@sentry/server-utils`'s emitted `build/`. Alias it to the source so
+// the tests run on a clean checkout without `@sentry/server-utils` having been built first.
+const orchestrionConfigSource = fileURLToPath(
+ new URL('../server-utils/src/orchestrion/config/index.ts', import.meta.url),
+);
+
+export default defineConfig({
+ ...baseConfig,
+ resolve: {
+ alias: {
+ '@sentry/server-utils/orchestrion/config': orchestrionConfigSource,
+ },
+ },
+ test: {
+ ...baseConfig.test,
+ },
+});
diff --git a/packages/server-utils/package.json b/packages/server-utils/package.json
index d95d222c5f3c..2245e6d65ebb 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/bun": {
+ "types": "./build/types/orchestrion/bundler/bun.d.ts",
+ "import": "./build/esm/orchestrion/bundler/bun.js",
+ "require": "./build/cjs/orchestrion/bundler/bun.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/bun": [
+ "build/types/orchestrion/bundler/bun.d.ts"
]
}
},
@@ -100,7 +94,6 @@
},
"devDependencies": {
"@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"
@@ -135,8 +128,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..675a81424abe 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`
@@ -105,22 +86,12 @@ export default [
'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',
'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..b9e18e4ce691
--- /dev/null
+++ b/packages/server-utils/src/orchestrion/bundler/bun.ts
@@ -0,0 +1,98 @@
+import codeTransformer from '@apm-js-collab/code-transformer-bundler-plugins/bun';
+import { INSTRUMENTED_MODULE_NAMES, withoutInstrumentedExternals } from '../config';
+import { ORCHESTRION_BUNDLER_MARKER_BANNER } from './moduleInjectedTransform';
+import type { PluginOptions } from './options';
+import { orchestrionTransformOptions } from './options';
+
+// 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(options: PluginOptions = {}): UnknownPlugin {
+ if (options.buildTimeInstrumentation === false) {
+ // Inert plugin — no banner, no force-bundling, no code transform — so SDKs that
+ // unconditionally push it into their plugin array can still opt out.
+ return { name: 'sentry-orchestrion-disabled', setup: () => undefined };
+ }
+
+ // Route through the shared assembly point so any future option reaches Bun too, but opt out of
+ // the transformer's own `injectDiagnostics` — Bun injects the marker banner via its native
+ // `banner` config below (which, unlike the upstream path, needs no `outdir`).
+ // 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(
+ orchestrionTransformOptions(options, { injectDiagnostics: false }),
+ ) 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);
+ },
+ };
+}
diff --git a/packages/server-utils/src/orchestrion/bundler/options.ts b/packages/server-utils/src/orchestrion/bundler/options.ts
index 510f924e1138..8f8a59f43140 100644
--- a/packages/server-utils/src/orchestrion/bundler/options.ts
+++ b/packages/server-utils/src/orchestrion/bundler/options.ts
@@ -55,17 +55,25 @@ export function externalizedModulesWarning(externalizedModules: string[]): strin
/**
* The `@apm-js-collab/code-transformer-bundler-plugins` options shared by every
- * orchestrion bundler plugin.
+ * orchestrion bundler plugin. This is the single assembly point for the plugin
+ * options, so a new field added here reaches every bundler at once.
*
* The module-injected `tracingChannelImport` override is always on: it is how
* every transformed module announces itself (and its channel-subscriber
* factory) at evaluation time, on every bundler. It is spread last so a user
* transform can't clobber it.
+ *
+ * `injectDiagnostics` returns the marker banner the upstream transformer prepends
+ * to the output. Bun opts out (`{ injectDiagnostics: false }`) because it injects
+ * the same banner via its native `banner` build config instead — see `bun.ts`.
*/
-export function orchestrionTransformOptions(options: PluginOptions): CodeTransformerPluginOptions {
+export function orchestrionTransformOptions(
+ options: PluginOptions,
+ { injectDiagnostics = true }: { injectDiagnostics?: boolean } = {},
+): CodeTransformerPluginOptions {
return {
instrumentations: [...SENTRY_INSTRUMENTATIONS, ...(options.instrumentations || [])],
customTransforms: { ...options.customTransforms, ...moduleInjectedTransforms() },
- injectDiagnostics: () => ORCHESTRION_BUNDLER_MARKER_BANNER,
+ ...(injectDiagnostics && { injectDiagnostics: () => ORCHESTRION_BUNDLER_MARKER_BANNER }),
};
}
diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts
index d105b967a72a..71b7d7100a8b 100644
--- a/packages/server-utils/src/orchestrion/config/index.ts
+++ b/packages/server-utils/src/orchestrion/config/index.ts
@@ -34,17 +34,18 @@ 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
* these are injected. The channel LISTENERS may live elsewhere (e.g. the NestJS
* one lives in `@sentry/nestjs`), but the config that decides what gets
* transformed is centralized here.
+ *
+ * This module MUST stay pure, stateless data. It is loaded in more than one realm
+ * at once — the build-time bundler plugin inlines it into the server chunk, while
+ * `@sentry/server-runtime-injection`'s external `register` loads its own copy from
+ * `node_modules` at runtime. Two identical, side-effect-free arrays are harmless;
+ * any module-level mutable state here would silently diverge between those copies.
*/
export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
...amqplibConfig,
diff --git a/packages/server-utils/test/orchestrion/bundler.test.ts b/packages/server-utils/test/orchestrion/bundler.test.ts
index 5649e3bf846b..3313b0872e2d 100644
--- a/packages/server-utils/test/orchestrion/bundler.test.ts
+++ b/packages/server-utils/test/orchestrion/bundler.test.ts
@@ -5,6 +5,7 @@ import type { NormalizedInputOptions, PluginContext } from 'rollup';
import type { ResolvedConfig } from 'vite';
import type { Compiler } from 'webpack';
import { describe, expect, it, vi } from 'vitest';
+import { sentryOrchestrionPlugin as bunPlugin } from '../../src/orchestrion/bundler/bun';
import { sentryOrchestrionPlugin as esbuildPlugin } from '../../src/orchestrion/bundler/esbuild';
import { orchestrionTransformOptions } from '../../src/orchestrion/bundler/options';
import { sentryOrchestrionPlugin as rollupPlugin } from '../../src/orchestrion/bundler/rollup';
@@ -25,6 +26,9 @@ vi.mock('@apm-js-collab/code-transformer-bundler-plugins/vite', () => ({
vi.mock('@apm-js-collab/code-transformer-bundler-plugins/webpack', () => ({
default: () => ({ apply: vi.fn() }),
}));
+vi.mock('@apm-js-collab/code-transformer-bundler-plugins/bun', () => ({
+ default: () => ({ name: 'code-transformer', setup: vi.fn() }),
+}));
describe('sentryOrchestrionPlugin (rollup)', () => {
// Mirrors what Rollup passes to buildStart: `external` is already normalized
@@ -292,18 +296,23 @@ describe('buildTimeInstrumentation: false', () => {
expect(() => sentryOrchestrionWebpackPlugin(disabled).apply(compiler)).not.toThrow();
expect(tap).not.toHaveBeenCalled();
});
+
+ it('returns an inert bun plugin that neither banners nor force-bundles', () => {
+ const build = { config: { banner: '', external: ['*'] } };
+ const plugin = bunPlugin(disabled);
+
+ expect(plugin.name).toBe('sentry-orchestrion-disabled');
+ expect(plugin.setup(build)).toBeUndefined();
+ expect(build.config.banner).toBe('');
+ expect(build.config.external).toEqual(['*']);
+ });
});
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);
@@ -314,7 +323,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', () => {
@@ -342,6 +351,18 @@ describe('orchestrionTransformOptions', () => {
expect(opts.customTransforms?.tracingChannelImport).not.toBe(clashing);
});
+ it('injects the marker banner by default', () => {
+ expect(orchestrionTransformOptions({}).injectDiagnostics).toBeTypeOf('function');
+ });
+
+ it('omits injectDiagnostics when opted out (Bun injects the banner natively instead)', () => {
+ const opts = orchestrionTransformOptions({}, { injectDiagnostics: false });
+
+ expect(opts.injectDiagnostics).toBeUndefined();
+ // The other options still come through the shared assembly point.
+ expect(typeof opts.customTransforms?.tracingChannelImport).toBe('function');
+ });
+
describe('marker banner', () => {
// Evaluate the emitted boot-banner snippet against a fake global, mirroring
// what runs when a bundled app boots. The banner only marks "the bundler
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"
},