From 702d59e722483e550872540b8d2e463b646c9442 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:20:05 +0200 Subject: [PATCH] fix(v10/server-runtime-injection): Load ESM hook without the build/esm package.json marker Backport of: #25562 Co-Authored-By: GPT-6 --- .../src/orchestrion/runtime/register.ts | 8 ++++ .../test/orchestrion/esmMarker.test.ts | 45 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 packages/server-utils/test/orchestrion/esmMarker.test.ts diff --git a/packages/server-utils/src/orchestrion/runtime/register.ts b/packages/server-utils/src/orchestrion/runtime/register.ts index 485d4613a1d8..38897391d475 100644 --- a/packages/server-utils/src/orchestrion/runtime/register.ts +++ b/packages/server-utils/src/orchestrion/runtime/register.ts @@ -105,6 +105,14 @@ export function registerDiagnosticsChannelInjection(_options?: RegisterDiagnosti // 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. + // Deployments can omit the package marker that tells Node these bundled hook files are ESM. + const esmDirUrl = new URL('../../../esm/', parentURL).href; + mod.register( + `data:text/javascript,${encodeURIComponent( + `const d=${JSON.stringify(esmDirUrl)};export async function load(u,c,n){return n(u,u.startsWith(d)?{...c,format:'module'}:c)}`, + )}`, + ); + mod.register('@sentry/server-utils/orchestrion/hook', { parentURL, data: { instrumentations: SENTRY_INSTRUMENTATIONS, diagnosticsPort }, diff --git a/packages/server-utils/test/orchestrion/esmMarker.test.ts b/packages/server-utils/test/orchestrion/esmMarker.test.ts new file mode 100644 index 000000000000..0af123f97dbf --- /dev/null +++ b/packages/server-utils/test/orchestrion/esmMarker.test.ts @@ -0,0 +1,45 @@ +import { spawnSync } from 'node:child_process'; +import { cpSync, mkdtempSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; + +/** + * Deployments can drop `build/esm/package.json` (the only `"type": "module"` marker for the hook), + * and Vercel runs Node with syntax detection disabled, so the loader thread then parses the hook as + * CommonJS. Runs against the built files, so it needs the package built. + * + * The copy sits inside this package so `@sentry/*` still resolves from its `node_modules`. + */ +const copyDir = mkdtempSync(join(__dirname, '.esm-marker-')); +cpSync(join(__dirname, '../..', 'build'), join(copyDir, 'build'), { recursive: true }); +cpSync(join(__dirname, '../..', 'package.json'), join(copyDir, 'package.json')); +rmSync(join(copyDir, 'build', 'esm', 'package.json')); + +afterAll(() => { + rmSync(copyDir, { recursive: true, force: true }); +}); + +describe('without the build/esm package.json marker', () => { + it('registers the ESM loader hook', { timeout: 30_000 }, () => { + // Removing `registerHooks` forces the `Module.register()` path, the one that loads the hook on + // Node < 24.13, whatever Node version runs the test. + const script = ` + require('node:module').registerHooks = undefined; + require('@sentry/core').debug.warn = (...args) => console.error(...args); + const { registerDiagnosticsChannelInjection } = require(${JSON.stringify(join(copyDir, 'build', 'cjs', 'orchestrion', 'runtime', 'register.js'))}); + registerDiagnosticsChannelInjection(); + import('data:text/javascript,export default 1').then(() => { + const marker = globalThis.__SENTRY__?.__SENTRY_ORCHESTRION__ ?? globalThis.__SENTRY_ORCHESTRION__; + process.stdout.write(JSON.stringify(marker)); + }); + `; + + const { stdout, stderr, status } = spawnSync(process.execPath, ['--no-experimental-detect-module', '-e', script], { + encoding: 'utf8', + }); + + expect(status, stderr).toBe(0); + expect(stderr).not.toContain('Failed to register diagnostics-channel injection hooks'); + expect(JSON.parse(stdout)).toEqual({ runtime: [] }); + }); +});