From 220ad816386a34e60ba6488cc484059eb915003f Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 14 Oct 2026 20:16:22 +0200 Subject: [PATCH] fix(node): Read module versions from the pnpm isolated layout `modulesIntegration` reads the version of each CJS module in `require.cache` from its package.json in a node_modules folder of the main module. The isolated pnpm layout keeps the package files in `node_modules/.pnpm//node_modules/`, so the integration found no version there. Events got only the version ranges from the app's package.json, and no packages that the app does not declare. Read these folders like the node_modules folders of the main module. The new test writes a package into this layout, because the monorepo installs its packages with the hoisted layout. Co-Authored-By: Claude Opus 5.5 --- .../suites/modules/scenario-isolated.js | 14 +++++++++++ .../suites/modules/test.ts | 23 +++++++++++++++++++ packages/node/src/integrations/modules.ts | 22 ++++++++++++++++-- 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/modules/scenario-isolated.js diff --git a/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js b/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js new file mode 100644 index 000000000000..ec543d39824e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js @@ -0,0 +1,14 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// `test.ts` writes these packages in the folders where the isolated layouts of pnpm and Bun keep them +require('./node_modules/.pnpm/pnpm-isolated-package@1.2.3/node_modules/pnpm-isolated-package'); +require('./node_modules/.bun/bun-isolated-package@4.5.6/node_modules/bun-isolated-package'); + +Sentry.captureException(new Error('error_1')); diff --git a/dev-packages/node-integration-tests/suites/modules/test.ts b/dev-packages/node-integration-tests/suites/modules/test.ts index f47c4c1f8b09..709bc24ddb1a 100644 --- a/dev-packages/node-integration-tests/suites/modules/test.ts +++ b/dev-packages/node-integration-tests/suites/modules/test.ts @@ -1,4 +1,5 @@ import { SDK_VERSION } from '@sentry/core'; +import { mkdirSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../utils/runner'; @@ -27,6 +28,28 @@ describe('modulesIntegration', () => { await runner.completed(); }); + test('CJS with the isolated layouts of pnpm and Bun', async () => { + const nodeModulesDir = join(__dirname, 'node_modules'); + for (const [storeDir, name, version] of [ + ['.pnpm', 'pnpm-isolated-package', '1.2.3'], + ['.bun', 'bun-isolated-package', '4.5.6'], + ] as const) { + const packageDir = join(nodeModulesDir, storeDir, `${name}@${version}`, 'node_modules', name); + mkdirSync(packageDir, { recursive: true }); + writeFileSync(join(packageDir, 'package.json'), JSON.stringify({ name, version })); + writeFileSync(join(packageDir, 'index.js'), 'module.exports = {};'); + } + + try { + await createRunner(__dirname, 'scenario-isolated.js') + .expect({ event: { modules: { 'pnpm-isolated-package': '1.2.3', 'bun-isolated-package': '4.5.6' } } }) + .start() + .completed(); + } finally { + rmSync(nodeModulesDir, { recursive: true, force: true }); + } + }); + test('ESM', async () => { const runner = createRunner(__dirname, 'server.mjs') .withInstrument(join(__dirname, 'instrument.mjs')) diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 508bae1beace..90921f75388b 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { basename, dirname, join } from 'node:path'; import { GLOBAL_OBJ, type IntegrationFn } from '@sentry/core'; type ModuleInfo = Record; @@ -73,6 +73,24 @@ function collectModules(): ModuleInfo { }; } +/** + * Whether `dir` is a node_modules folder of the main module, or a folder + * `/<.pnpm or .bun>//node_modules`, where the isolated layouts of pnpm and Bun keep + * each package. + */ +function isMainModulesDir(dir: string, mainPaths: string[]): boolean { + if (mainPaths.includes(dir)) { + return true; + } + + const storeDir = dirname(dirname(dir)); + return ( + basename(dir) === 'node_modules' && + ['.pnpm', '.bun'].includes(basename(storeDir)) && + mainPaths.includes(dirname(storeDir)) + ); +} + /** Extract information about package.json modules from require.cache */ function collectRequireModules(): ModuleInfo { const mainPaths = require.main?.paths || []; @@ -94,7 +112,7 @@ function collectRequireModules(): ModuleInfo { if (!dir || orig === dir || seen.has(orig)) { return undefined; } - if (mainPaths.indexOf(dir) < 0) { + if (!isMainModulesDir(dir, mainPaths)) { return updir(); }