diff --git a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs index 544c773e5e7c..caaf73162ded 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs +++ b/dev-packages/e2e-tests/test-applications/node-express-esm-loader/src/instrument.mjs @@ -5,4 +5,5 @@ Sentry.init({ dsn: process.env.E2E_TEST_DSN, tunnel: `http://localhost:3031/`, // proxy server tracesSampleRate: 1, + registerEsmLoaderHooks: { onlyIncludeInstrumentedModules: true }, }); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs new file mode 100644 index 000000000000..6b20155aea38 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/app.mjs @@ -0,0 +1,21 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; +import * as iitm from 'import-in-the-middle'; + +new iitm.Hook((_, name) => { + if (name !== 'http') { + throw new Error(`'http' should be the only hooked modules but we just hooked '${name}'`); + } +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + autoSessionTracking: false, + transport: loggingTransport, + registerEsmLoaderHooks: { onlyIncludeInstrumentedModules: true }, +}); + +await import('./sub-module.mjs'); +await import('http'); +await import('os'); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs new file mode 100644 index 000000000000..9940c57857eb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.assert(true); diff --git a/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts new file mode 100644 index 000000000000..8b9e6e06202f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/esm/import-in-the-middle/test.ts @@ -0,0 +1,12 @@ +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +conditionalTest({ min: 18 })('import-in-the-middle', () => { + test('onlyIncludeInstrumentedModules', done => { + createRunner(__dirname, 'app.mjs').ensureNoErrorOutput().start(done); + }); +}); diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 03d8cea76fac..37b94ebc439f 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -10,6 +10,7 @@ import { import { SDK_VERSION } from '@sentry/core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { GLOBAL_OBJ, consoleSandbox, logger } from '@sentry/utils'; +import { createAddHookMessageChannel } from 'import-in-the-middle'; import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; import { SentryContextManager } from '../otel/contextManager'; @@ -31,6 +32,26 @@ export function initOpenTelemetry(client: NodeClient): void { client.traceProvider = provider; } +type ImportInTheMiddleInitData = Pick & { + addHookMessagePort?: unknown; +}; + +interface RegisterOptions { + data?: ImportInTheMiddleInitData; + transferList?: unknown[]; +} + +function getRegisterOptions(esmHookConfig?: EsmLoaderHookOptions): RegisterOptions { + if (esmHookConfig?.onlyIncludeInstrumentedModules) { + const { addHookMessagePort } = createAddHookMessageChannel(); + // If the user supplied include, we need to use that as a starting point or use an empty array to ensure no modules + // are wrapped if they are not hooked + return { data: { addHookMessagePort, include: esmHookConfig.include || [] }, transferList: [addHookMessagePort] }; + } + + return { data: esmHookConfig }; +} + /** Initialize the ESM loader. */ export function maybeInitializeEsmLoader(esmHookConfig?: EsmLoaderHookOptions): void { const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split('.').map(Number); @@ -44,7 +65,7 @@ export function maybeInitializeEsmLoader(esmHookConfig?: EsmLoaderHookOptions): if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered && importMetaUrl) { try { // @ts-expect-error register is available in these versions - moduleModule.register('import-in-the-middle/hook.mjs', importMetaUrl, { data: esmHookConfig }); + moduleModule.register('import-in-the-middle/hook.mjs', importMetaUrl, getRegisterOptions(esmHookConfig)); GLOBAL_OBJ._sentryEsmLoaderHookRegistered = true; } catch (error) { logger.warn('Failed to register ESM hook', error); diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 9604b31ddb22..aa9873e2da91 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -6,7 +6,18 @@ import type { NodeTransportOptions } from './transports'; export interface EsmLoaderHookOptions { include?: Array; - exclude?: Array; + exclude?: Array /** + * When set to `true`, `import-in-the-middle` will only wrap ESM modules that are specifically instrumented by + * OpenTelemetry plugins. This is useful to avoid issues where `import-in-the-middle` is not compatible with some of + * your dependencies. + * + * **Note**: This feature will only work if you `Sentry.init()` the SDK before the instrumented modules are loaded. + * This can be achieved via the Node `--import` CLI flag or by loading your app via async `import()` after calling + * `Sentry.init()`. + * + * Defaults to `false`. + */; + onlyIncludeInstrumentedModules?: boolean; } export interface BaseNodeOptions {