diff --git a/packages/node/src/nodeVersion.ts b/packages/node/src/nodeVersion.ts index 1f07883b771b..7f2eaeb6c4b1 100644 --- a/packages/node/src/nodeVersion.ts +++ b/packages/node/src/nodeVersion.ts @@ -2,3 +2,4 @@ import { parseSemver } from '@sentry/utils'; export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; export const NODE_MAJOR = NODE_VERSION.major; +export const NODE_MINOR = NODE_VERSION.minor; diff --git a/packages/node/src/sdk/init.ts b/packages/node/src/sdk/init.ts index 82cd26492960..4250f5de9828 100644 --- a/packages/node/src/sdk/init.ts +++ b/packages/node/src/sdk/init.ts @@ -14,7 +14,6 @@ import { import { openTelemetrySetupCheck, setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; import type { Client, Integration, Options } from '@sentry/types'; import { - GLOBAL_OBJ, consoleSandbox, dropUndefinedKeys, logger, @@ -26,7 +25,6 @@ import { consoleIntegration } from '../integrations/console'; import { nodeContextIntegration } from '../integrations/context'; import { contextLinesIntegration } from '../integrations/contextlines'; -import moduleModule from 'module'; import { httpIntegration } from '../integrations/http'; import { localVariablesIntegration } from '../integrations/local-variables'; import { modulesIntegration } from '../integrations/modules'; @@ -41,6 +39,7 @@ import { isCjs } from '../utils/commonjs'; import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initOpenTelemetry } from './initOtel'; +import { registerEsmModuleHooks } from './register-hooks'; function getCjsOnlyIntegrations(): Integration[] { return isCjs() ? [modulesIntegration()] : []; @@ -92,8 +91,6 @@ function shouldAddPerformanceIntegrations(options: Options): boolean { return options.enableTracing || options.tracesSampleRate != null || 'tracesSampler' in options; } -declare const __IMPORT_META_URL_REPLACEMENT__: string; - /** * Initialize Sentry for Node. */ @@ -130,31 +127,27 @@ function _init( } if (!isCjs()) { - const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number); - - // Register hook was added in v20.6.0 and v18.19.0 - if (nodeMajor >= 22 || (nodeMajor === 20 && nodeMinor >= 6) || (nodeMajor === 18 && nodeMinor >= 19)) { - // We need to work around using import.meta.url directly because jest complains about it. - const importMetaUrl = - typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' ? __IMPORT_META_URL_REPLACEMENT__ : undefined; - - if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered && importMetaUrl) { - try { - // @ts-expect-error register is available in these versions - moduleModule.register('@opentelemetry/instrumentation/hook.mjs', importMetaUrl); - GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; - } catch (error) { - logger.warn('Failed to register ESM hook', error); - } - } - } else { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', - ); - }); - } + registerEsmModuleHooks([ + 'http', + 'node:http', + 'https', + 'node:https', + 'connect', + 'express', + 'fastify', + 'graphql', + '@hapi/hapi', + 'ioredis', + 'koa', + 'mongodb', + 'mongoose', + 'mysql', + 'mysql2', + 'pg', + '@nestjs/core', + 'prisma', + ...(options._experiments?.modulesToInstrument || []), + ]); } setOpenTelemetryContextAsyncContextStrategy(); diff --git a/packages/node/src/sdk/register-hooks.ts b/packages/node/src/sdk/register-hooks.ts new file mode 100644 index 000000000000..bf22c39aac56 --- /dev/null +++ b/packages/node/src/sdk/register-hooks.ts @@ -0,0 +1,73 @@ +import * as mod from 'node:module'; +import { GLOBAL_OBJ, consoleSandbox, logger } from '@sentry/utils'; +import { NODE_MAJOR, NODE_MINOR } from '../nodeVersion'; + +declare const __IMPORT_META_URL_REPLACEMENT__: string; + +/** + * Registers hooks for ESM modules. + * + * The first hook overrides the source code of 'import-in-the-middle/hook.mjs' so it only instruments pre-specified modules. + */ +export function registerEsmModuleHooks(modulesToInstrument: string[]): void { + // Register hook was added in v20.6.0 and v18.19.0 + if (NODE_MAJOR >= 22 || (NODE_MAJOR === 20 && NODE_MINOR >= 6) || (NODE_MAJOR === 18 && NODE_MINOR >= 19)) { + // We need to work around using import.meta.url directly because jest complains about it. + const importMetaUrl = + typeof __IMPORT_META_URL_REPLACEMENT__ !== 'undefined' ? __IMPORT_META_URL_REPLACEMENT__ : undefined; + + if (!importMetaUrl || GLOBAL_OBJ._sentryEsmLoaderHookRegistered) { + return; + } + + try { + const iitmOverrideCode = ` + import { createHook } from "./hook.js"; + + const { load, resolve: resolveIITM, getFormat, getSource } = createHook(import.meta); + + const modulesToInstrument = ${JSON.stringify(modulesToInstrument)}; + + export async function resolve(specifier, context, nextResolve) { + if (modulesToInstrument.includes(specifier)) { + return resolveIITM(specifier, context, nextResolve); + } + + return nextResolve(specifier, context); + } + + export { load, getFormat, getSource };`; + + // This loader overrides the source code of 'import-in-the-middle/hook.mjs' with + // the above code. This code ensures that only the specified modules are proxied. + // @ts-expect-error register is available in these versions + mod.register( + new URL( + `data:application/javascript, + export async function load(url, context, nextLoad) { + if (url.endsWith('import-in-the-middle/hook.mjs')) { + const result = await nextLoad(url, context); + return {...result, source: '${iitmOverrideCode}' }; + } + return nextLoad(url, context); + }`, + ), + importMetaUrl, + ); + + // @ts-expect-error register is available in these versions + mod.register('@opentelemetry/instrumentation/hook.mjs', importMetaUrl); + + GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; + } catch (error) { + logger.warn('Failed to register ESM hook', error); + } + } else { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', + ); + }); + } +}