Skip to content

Commit 00ee6b7

Browse files
andreiborzaclaude
andauthored
fix(server-runtime-injection): Load ESM hook without the build/esm package.json marker (#25562)
## What The diagnostics-channel hook now loads as ESM even when `build/esm/package.json` is missing from a deployment. ## Why Vercel runs Node 22 with syntax detection off, so without that file the hook failed to load and channel-based spans were lost. Closes: #25480 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3fdf3c4 commit 00ee6b7

2 files changed

Lines changed: 61 additions & 0 deletions

File tree

‎packages/server-runtime-injection/src/register.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,25 @@ export function registerDiagnosticsChannelInjection(): void {
321321

322322
const hookSpecifier = hookFound ? pathToFileURL(hookPath).href : '@sentry/server-runtime-injection/hook';
323323

324+
if (hookFound) {
325+
// Only `build/esm/package.json` marks the hook and its chunks as ESM. Deployments can drop
326+
// that file, and Vercel disables syntax detection, so Node then parses the hook as CommonJS
327+
// and fails on its `export`s.
328+
//
329+
// This registers a small loader hook before the real one. Its `load` sets
330+
// `format: 'module'` for every URL inside our ESM build directory and passes all other URLs
331+
// through unchanged. Hooks registered earlier also apply when Node loads hook modules
332+
// registered later, so the real hook and its chunks load as ESM even without the marker.
333+
// The loader is inlined as a `data:` URL because Node always loads those as ESM, so it
334+
// cannot fail the same way and needs no file of its own.
335+
const esmDirUrl = `${pathToFileURL(dirname(hookPath)).href}/`;
336+
mod.register(
337+
`data:text/javascript,${encodeURIComponent(
338+
`const d=${JSON.stringify(esmDirUrl)};export async function load(u,c,n){return n(u,u.startsWith(d)?{...c,format:'module'}:c)}`,
339+
)}`,
340+
);
341+
}
342+
324343
mod.register(hookSpecifier, {
325344
parentURL,
326345
data: { instrumentations: SENTRY_RUNTIME_INSTRUMENTATIONS, diagnosticsPort },
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { cpSync, mkdtempSync, rmSync } from 'node:fs';
3+
import { join } from 'node:path';
4+
import { afterAll, describe, expect, it } from 'vitest';
5+
6+
/**
7+
* Deployments can drop `build/esm/package.json` (the only `"type": "module"` marker for the hook),
8+
* and Vercel runs Node with syntax detection disabled, so the loader thread then parses the hook as
9+
* CommonJS. Runs against the built files, so it needs the package built.
10+
*
11+
* The copy sits inside this package so `@sentry/*` still resolves from its `node_modules`.
12+
*/
13+
const copyDir = mkdtempSync(join(__dirname, '.esm-marker-'));
14+
cpSync(join(__dirname, '..', 'build'), join(copyDir, 'build'), { recursive: true });
15+
rmSync(join(copyDir, 'build', 'esm', 'package.json'));
16+
17+
afterAll(() => {
18+
rmSync(copyDir, { recursive: true, force: true });
19+
});
20+
21+
describe('without the build/esm package.json marker', () => {
22+
it('registers the ESM loader hook', { timeout: 30_000 }, () => {
23+
// Removing `registerHooks` forces the `Module.register()` path, the one that loads the hook on
24+
// Node < 24.13, whatever Node version runs the test.
25+
const script = `
26+
require('node:module').registerHooks = undefined;
27+
const { registerDiagnosticsChannelInjection } = require(${JSON.stringify(join(copyDir, 'build', 'cjs', 'register.js'))});
28+
registerDiagnosticsChannelInjection();
29+
import('data:text/javascript,export default 1').then(() => {
30+
const marker = globalThis.__SENTRY__?.__SENTRY_ORCHESTRION__ ?? globalThis.__SENTRY_ORCHESTRION__;
31+
process.stdout.write(JSON.stringify(marker));
32+
});
33+
`;
34+
35+
const { stdout, stderr } = spawnSync(process.execPath, ['--no-experimental-detect-module', '-e', script], {
36+
encoding: 'utf8',
37+
});
38+
39+
expect(stderr).not.toContain('Failed to register diagnostics-channel injection hooks');
40+
expect(JSON.parse(stdout)).toEqual({ runtime: [] });
41+
});
42+
});

0 commit comments

Comments
 (0)