Skip to content

feat(node): Ensure modulesIntegration works in more environments #16566

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,12 @@ test('Should capture an error and transaction for a app router page', async ({ p
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();

// Modules are set for Next.js
expect(errorEvent.modules).toEqual(
expect.objectContaining({
'@sentry/nextjs': expect.any(String),
'@playwright/test': expect.any(String),
}),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
transport: loggingTransport,
});
22 changes: 22 additions & 0 deletions dev-packages/node-integration-tests/suites/modules/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
transport: loggingTransport,
});

// express must be required after Sentry is initialized
const express = require('express');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.get('/test1', () => {
throw new Error('error_1');
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
13 changes: 13 additions & 0 deletions dev-packages/node-integration-tests/suites/modules/server.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from '@sentry/node';
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
import express from 'express';

const app = express();

app.get('/test1', () => {
throw new Error('error_1');
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
48 changes: 48 additions & 0 deletions dev-packages/node-integration-tests/suites/modules/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { SDK_VERSION } from '@sentry/core';
import { join } from 'path';
import { afterAll, describe, test } from 'vitest';
import { cleanupChildProcesses, createRunner } from '../../utils/runner';

describe('modulesIntegration', () => {
afterAll(() => {
cleanupChildProcesses();
});

test('CJS', async () => {
const runner = createRunner(__dirname, 'server.js')
.withMockSentryServer()
.expect({
event: {
modules: {
// exact version comes from require.caches
express: '4.21.1',
// this comes from package.json
'@sentry/node': SDK_VERSION,
yargs: '^16.2.0',
},
},
})
.start();
runner.makeRequest('get', '/test1', { expectError: true });
await runner.completed();
});

test('ESM', async () => {
const runner = createRunner(__dirname, 'server.mjs')
.withInstrument(join(__dirname, 'instrument.mjs'))
.withMockSentryServer()
.expect({
event: {
modules: {
// this comes from package.json
express: '^4.21.1',
'@sentry/node': SDK_VERSION,
yargs: '^16.2.0',
},
},
})
.start();
runner.makeRequest('get', '/test1', { expectError: true });
await runner.completed();
});
});
26 changes: 26 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,14 @@ export function constructWebpackConfigFunction(
);
}

// We inject a map of dependencies that the nextjs app has, as we cannot reliably extract them at runtime, sadly
newConfig.plugins = newConfig.plugins || [];
newConfig.plugins.push(
new buildContext.webpack.DefinePlugin({
__SENTRY_SERVER_MODULES__: JSON.stringify(_getModules(projectDir)),
}),
);

return newConfig;
};
}
Expand Down Expand Up @@ -825,3 +833,21 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
newConfig.ignoreWarnings.push(...ignoreRules);
}
}

function _getModules(projectDir: string): Record<string, string> {
try {
const packageJson = path.join(projectDir, 'package.json');
const packageJsonContent = fs.readFileSync(packageJson, 'utf8');
const packageJsonObject = JSON.parse(packageJsonContent) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};

return {
...packageJsonObject.dependencies,
...packageJsonObject.devDependencies,
};
} catch {
return {};
}
}
92 changes: 61 additions & 31 deletions packages/node/src/integrations/modules.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import type { IntegrationFn } from '@sentry/core';
import { defineIntegration, logger } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { defineIntegration } from '@sentry/core';
import { isCjs } from '../utils/commonjs';

let moduleCache: { [key: string]: string };
type ModuleInfo = Record<string, string>;

let moduleCache: ModuleInfo | undefined;

const INTEGRATION_NAME = 'Modules';

const _modulesIntegration = (() => {
// This integration only works in CJS contexts
if (!isCjs()) {
DEBUG_BUILD &&
logger.warn(
'modulesIntegration only works in CommonJS (CJS) environments. Remove this integration if you are using ESM.',
);
return {
name: INTEGRATION_NAME,
};
}
declare const __SENTRY_SERVER_MODULES__: Record<string, string>;

/**
* `__SENTRY_SERVER_MODULES__` can be replaced at build time with the modules loaded by the server.
* Right now, we leverage this in Next.js to circumvent the problem that we do not get access to these things at runtime.
*/
const SERVER_MODULES = typeof __SENTRY_SERVER_MODULES__ === 'undefined' ? {} : __SENTRY_SERVER_MODULES__;

const _modulesIntegration = (() => {
return {
name: INTEGRATION_NAME,
processEvent(event) {
Expand All @@ -36,13 +34,14 @@ const _modulesIntegration = (() => {

/**
* Add node modules / packages to the event.
*
* Only works in CommonJS (CJS) environments.
* For this, multiple sources are used:
* - They can be injected at build time into the __SENTRY_SERVER_MODULES__ variable (e.g. in Next.js)
* - They are extracted from the dependencies & devDependencies in the package.json file
* - They are extracted from the require.cache (CJS only)
*/
export const modulesIntegration = defineIntegration(_modulesIntegration);

/** Extract information about paths */
function getPaths(): string[] {
function getRequireCachePaths(): string[] {
try {
return require.cache ? Object.keys(require.cache as Record<string, unknown>) : [];
} catch (e) {
Expand All @@ -51,17 +50,23 @@ function getPaths(): string[] {
}

/** Extract information about package.json modules */
function collectModules(): {
[name: string]: string;
} {
function collectModules(): ModuleInfo {
return {
...SERVER_MODULES,
...getModulesFromPackageJson(),
...(isCjs() ? collectRequireModules() : {}),
};
}

/** Extract information about package.json modules from require.cache */
function collectRequireModules(): ModuleInfo {
const mainPaths = require.main?.paths || [];
const paths = getPaths();
const infos: {
[name: string]: string;
} = {};
const seen: {
[path: string]: boolean;
} = {};
const paths = getRequireCachePaths();

// 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 seen = new Set<string>();

paths.forEach(path => {
let dir = path;
Expand All @@ -71,15 +76,15 @@ function collectModules(): {
const orig = dir;
dir = dirname(orig);

if (!dir || orig === dir || seen[orig]) {
if (!dir || orig === dir || seen.has(orig)) {
return undefined;
}
if (mainPaths.indexOf(dir) < 0) {
return updir();
}

const pkgfile = join(orig, 'package.json');
seen[orig] = true;
seen.add(orig);

if (!existsSync(pkgfile)) {
return updir();
Expand All @@ -103,9 +108,34 @@ function collectModules(): {
}

/** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */
function _getModules(): { [key: string]: string } {
function _getModules(): ModuleInfo {
if (!moduleCache) {
moduleCache = collectModules();
}
return moduleCache;
}

interface PackageJson {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
}

function getPackageJson(): PackageJson {
try {
const filePath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(readFileSync(filePath, 'utf8')) as PackageJson;

return packageJson;
} catch (e) {
return {};
}
}

function getModulesFromPackageJson(): ModuleInfo {
const packageJson = getPackageJson();

return {
...packageJson.dependencies,
...packageJson.devDependencies,
Comment on lines +138 to +139
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l/maybe something to follow up on in the future: Would be cool to add some kind of flag to differ between dependencies and devDependencies. Doesn't have to happen now of course

};
}
6 changes: 1 addition & 5 deletions packages/node/src/sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ import { defaultStackParser, getSentryRelease } from './api';
import { NodeClient } from './client';
import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel';

function getCjsOnlyIntegrations(): Integration[] {
return isCjs() ? [modulesIntegration()] : [];
}

/**
* Get default integrations, excluding performance.
*/
Expand All @@ -69,7 +65,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
nodeContextIntegration(),
childProcessIntegration(),
processSessionIntegration(),
...getCjsOnlyIntegrations(),
modulesIntegration(),
];
}

Expand Down
Loading