From d7c2035d2bdc206f4d0f946a65d37bbe13681ff3 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 14 Oct 2026 20:16:22 +0200 Subject: [PATCH 1/2] 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(); } From 7a758c55cb075339ede5934b4a34e11107a04a1e Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 21 Oct 2026 19:03:06 +0200 Subject: [PATCH 2/2] fixup! fix(node): Read module versions from the pnpm isolated layout --- .../suites/modules/scenario-isolated.js | 8 +++-- .../suites/modules/test.ts | 32 +++++++++++++++---- packages/node/src/integrations/modules.ts | 24 +++++++++++--- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js b/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js index ec543d39824e..29f9e62decca 100644 --- a/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js +++ b/dev-packages/node-integration-tests/suites/modules/scenario-isolated.js @@ -8,7 +8,11 @@ Sentry.init({ }); // `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'); +require('pnpm-isolated-package'); +require('@pnpm-isolated/scoped-package'); +require('bun-isolated-package'); +require('@bun-isolated/scoped-package'); +// Another version of `pnpm-isolated-package`, loaded last as a dependency of the app would load it +require('./node_modules/.pnpm/pnpm-isolated-package@1.0.0/node_modules/pnpm-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 709bc24ddb1a..57c221f9f7f0 100644 --- a/dev-packages/node-integration-tests/suites/modules/test.ts +++ b/dev-packages/node-integration-tests/suites/modules/test.ts @@ -1,6 +1,6 @@ import { SDK_VERSION } from '@sentry/core'; -import { mkdirSync, rmSync, writeFileSync } from 'fs'; -import { join } from 'path'; +import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; import { afterAll, describe, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../utils/runner'; @@ -30,19 +30,37 @@ describe('modulesIntegration', () => { 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'], + for (const [storeDir, name, version, linked] of [ + ['.pnpm', 'pnpm-isolated-package', '1.2.3', true], + // Only a dependency of the app uses this version, so it is not linked into `node_modules` + ['.pnpm', 'pnpm-isolated-package', '1.0.0', false], + ['.pnpm', '@pnpm-isolated/scoped-package', '2.3.4', true], + ['.bun', 'bun-isolated-package', '4.5.6', true], + ['.bun', '@bun-isolated/scoped-package', '5.6.7', true], ] as const) { - const packageDir = join(nodeModulesDir, storeDir, `${name}@${version}`, 'node_modules', name); + const packageDir = join(nodeModulesDir, storeDir, `${name.replace('/', '+')}@${version}`, 'node_modules', name); mkdirSync(packageDir, { recursive: true }); writeFileSync(join(packageDir, 'package.json'), JSON.stringify({ name, version })); writeFileSync(join(packageDir, 'index.js'), 'module.exports = {};'); + + if (linked) { + mkdirSync(dirname(join(nodeModulesDir, name)), { recursive: true }); + symlinkSync(packageDir, join(nodeModulesDir, name), 'junction'); + } } try { await createRunner(__dirname, 'scenario-isolated.js') - .expect({ event: { modules: { 'pnpm-isolated-package': '1.2.3', 'bun-isolated-package': '4.5.6' } } }) + .expect({ + event: { + modules: { + 'pnpm-isolated-package': '1.2.3', + '@pnpm-isolated/scoped-package': '2.3.4', + 'bun-isolated-package': '4.5.6', + '@bun-isolated/scoped-package': '5.6.7', + }, + }, + }) .start() .completed(); } finally { diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 90921f75388b..54842d0cd5a3 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync, realpathSync } from 'node:fs'; import { basename, dirname, join } from 'node:path'; import { GLOBAL_OBJ, type IntegrationFn } from '@sentry/core'; @@ -74,11 +74,15 @@ function collectModules(): ModuleInfo { } /** - * Whether `dir` is a node_modules folder of the main module, or a folder + * Whether `dir` is a node_modules folder of the main module, a folder * `/<.pnpm or .bun>//node_modules`, where the isolated layouts of pnpm and Bun keep - * each package. + * each package, or a scope folder (`@scope`) in one of these folders. */ function isMainModulesDir(dir: string, mainPaths: string[]): boolean { + if (basename(dir).startsWith('@')) { + return isMainModulesDir(dirname(dir), mainPaths); + } + if (mainPaths.includes(dir)) { return true; } @@ -91,6 +95,12 @@ function isMainModulesDir(dir: string, mainPaths: string[]): boolean { ); } +/** Whether `require(name)` in the main module loads the package in `packageDir`. */ +function isResolvedFromMain(name: string, packageDir: string, mainPaths: string[]): boolean { + const resolvedDir = mainPaths.map(dir => join(dir, name)).find(dir => existsSync(dir)); + return !!resolvedDir && realpathSync(resolvedDir) === realpathSync(packageDir); +} + /** Extract information about package.json modules from require.cache */ function collectRequireModules(): ModuleInfo { const mainPaths = require.main?.paths || []; @@ -99,6 +109,7 @@ function collectRequireModules(): ModuleInfo { // We start with the modules from package.json (if possible) // These may be overwritten by more specific versions from the require.cache const infos: ModuleInfo = {}; + const packageDirs: Record = {}; const seen = new Set(); paths.forEach(path => { @@ -128,7 +139,12 @@ function collectRequireModules(): ModuleInfo { name: string; version: string; }; - infos[info.name] = info.version; + // The isolated layouts of pnpm and Bun keep every version of a package. Keep the one that the main module loads. + const previousDir = packageDirs[info.name]; + if (!previousDir || !isResolvedFromMain(info.name, previousDir, mainPaths)) { + infos[info.name] = info.version; + packageDirs[info.name] = orig; + } } catch { // no-empty }