Skip to content

Commit a8ae75b

Browse files
committed
feat(node): Ensure modulesIntegration works in more environments
1 parent 038cb5d commit a8ae75b

File tree

8 files changed

+186
-36
lines changed

8 files changed

+186
-36
lines changed

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,12 @@ test('Should capture an error and transaction for a app router page', async ({ p
123123
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
124124
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
125125
expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
126+
127+
// Modules are set for Next.js
128+
expect(errorEvent.modules).toEqual(
129+
expect.objectContaining({
130+
'@sentry/nextjs': expect.any(String),
131+
'@playwright/test': expect.any(String),
132+
}),
133+
);
126134
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
transport: loggingTransport,
8+
});
9+
10+
// express must be required after Sentry is initialized
11+
const express = require('express');
12+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
13+
14+
const app = express();
15+
16+
app.get('/test1', (_req, _res) => {
17+
throw new Error('error_1');
18+
});
19+
20+
Sentry.setupExpressErrorHandler(app);
21+
22+
startExpressServerAndSendPortToRunner(app);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as Sentry from '@sentry/node';
2+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
import express from 'express';
4+
5+
const app = express();
6+
7+
app.get('/test1', (_req, _res) => {
8+
throw new Error('error_1');
9+
});
10+
11+
Sentry.setupExpressErrorHandler(app);
12+
13+
startExpressServerAndSendPortToRunner(app);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { SDK_VERSION } from '@sentry/core';
2+
import { join } from 'path';
3+
import { afterAll, describe, expect, test } from 'vitest';
4+
import { cleanupChildProcesses, createRunner } from '../../utils/runner';
5+
6+
describe('modulesIntegration', () => {
7+
afterAll(() => {
8+
cleanupChildProcesses();
9+
});
10+
11+
test('CJS', async () => {
12+
const runner = createRunner(__dirname, 'server.js')
13+
.withMockSentryServer()
14+
.expect({
15+
event: {
16+
modules: {
17+
// exact version comes from require.caches
18+
express: '4.21.1',
19+
// this comes from package.json
20+
'@sentry/node': SDK_VERSION,
21+
yargs: '^16.2.0',
22+
},
23+
},
24+
})
25+
.start();
26+
runner.makeRequest('get', '/test1', { expectError: true });
27+
await runner.completed();
28+
});
29+
30+
test('ESM', async () => {
31+
const runner = createRunner(__dirname, 'server.mjs')
32+
.withInstrument(join(__dirname, 'instrument.mjs'))
33+
.withMockSentryServer()
34+
.expect({
35+
event: {
36+
modules: {
37+
// this comes from package.json
38+
express: '^4.21.1',
39+
'@sentry/node': SDK_VERSION,
40+
yargs: '^16.2.0',
41+
},
42+
},
43+
})
44+
.start();
45+
runner.makeRequest('get', '/test1', { expectError: true });
46+
await runner.completed();
47+
});
48+
});

packages/nextjs/src/config/webpack.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,14 @@ export function constructWebpackConfigFunction(
410410
);
411411
}
412412

413+
// We inject a map of dependencies that the nextjs app has, as we cannot reliably extract them at runtime, sadly
414+
newConfig.plugins = newConfig.plugins || [];
415+
newConfig.plugins.push(
416+
new buildContext.webpack.DefinePlugin({
417+
__SENTRY_SERVER_MODULES__: JSON.stringify(_getModules(projectDir)),
418+
}),
419+
);
420+
413421
return newConfig;
414422
};
415423
}
@@ -825,3 +833,21 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules)
825833
newConfig.ignoreWarnings.push(...ignoreRules);
826834
}
827835
}
836+
837+
function _getModules(projectDir: string): Record<string, string> {
838+
try {
839+
const packageJson = path.join(projectDir, 'package.json');
840+
const packageJsonContent = fs.readFileSync(packageJson, 'utf8');
841+
const packageJsonObject = JSON.parse(packageJsonContent) as {
842+
dependencies?: Record<string, string>;
843+
devDependencies?: Record<string, string>;
844+
};
845+
846+
return {
847+
...packageJsonObject.dependencies,
848+
...packageJsonObject.devDependencies,
849+
};
850+
} catch {
851+
return {};
852+
}
853+
}
Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import { existsSync, readFileSync } from 'node:fs';
22
import { dirname, join } from 'node:path';
33
import type { IntegrationFn } from '@sentry/core';
4-
import { defineIntegration, logger } from '@sentry/core';
5-
import { DEBUG_BUILD } from '../debug-build';
4+
import { defineIntegration } from '@sentry/core';
65
import { isCjs } from '../utils/commonjs';
76

8-
let moduleCache: { [key: string]: string };
7+
type ModuleInfo = Record<string, string>;
8+
9+
let moduleCache: ModuleInfo | undefined;
910

1011
const INTEGRATION_NAME = 'Modules';
1112

12-
const _modulesIntegration = (() => {
13-
// This integration only works in CJS contexts
14-
if (!isCjs()) {
15-
DEBUG_BUILD &&
16-
logger.warn(
17-
'modulesIntegration only works in CommonJS (CJS) environments. Remove this integration if you are using ESM.',
18-
);
19-
return {
20-
name: INTEGRATION_NAME,
21-
};
22-
}
13+
declare const __SENTRY_SERVER_MODULES__: Record<string, string>;
14+
15+
/**
16+
* This is replaced at build time with the modules loaded by the server.
17+
*/
18+
const SERVER_MODULES = typeof __SENTRY_SERVER_MODULES__ === 'undefined' ? {} : __SENTRY_SERVER_MODULES__;
2319

20+
const _modulesIntegration = (() => {
2421
return {
2522
name: INTEGRATION_NAME,
2623
processEvent(event) {
@@ -36,13 +33,14 @@ const _modulesIntegration = (() => {
3633

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

44-
/** Extract information about paths */
45-
function getPaths(): string[] {
43+
function getRequireCachePaths(): string[] {
4644
try {
4745
return require.cache ? Object.keys(require.cache as Record<string, unknown>) : [];
4846
} catch (e) {
@@ -51,17 +49,23 @@ function getPaths(): string[] {
5149
}
5250

5351
/** Extract information about package.json modules */
54-
function collectModules(): {
55-
[name: string]: string;
56-
} {
52+
function collectModules(): ModuleInfo {
53+
return {
54+
...SERVER_MODULES,
55+
...getModulesFromPackageJson(),
56+
...(isCjs() ? collectRequireModules() : {}),
57+
};
58+
}
59+
60+
/** Extract information about package.json modules from require.cache */
61+
function collectRequireModules(): ModuleInfo {
5762
const mainPaths = require.main?.paths || [];
58-
const paths = getPaths();
59-
const infos: {
60-
[name: string]: string;
61-
} = {};
62-
const seen: {
63-
[path: string]: boolean;
64-
} = {};
63+
const paths = getRequireCachePaths();
64+
65+
// We start with the modules from package.json (if possible)
66+
// These may be overwritten by more specific versions from the require.cache
67+
const infos: ModuleInfo = {};
68+
const seen = new Set<string>();
6569

6670
paths.forEach(path => {
6771
let dir = path;
@@ -71,15 +75,15 @@ function collectModules(): {
7175
const orig = dir;
7276
dir = dirname(orig);
7377

74-
if (!dir || orig === dir || seen[orig]) {
78+
if (!dir || orig === dir || seen.has(orig)) {
7579
return undefined;
7680
}
7781
if (mainPaths.indexOf(dir) < 0) {
7882
return updir();
7983
}
8084

8185
const pkgfile = join(orig, 'package.json');
82-
seen[orig] = true;
86+
seen.add(orig);
8387

8488
if (!existsSync(pkgfile)) {
8589
return updir();
@@ -103,9 +107,34 @@ function collectModules(): {
103107
}
104108

105109
/** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */
106-
function _getModules(): { [key: string]: string } {
110+
function _getModules(): ModuleInfo {
107111
if (!moduleCache) {
108112
moduleCache = collectModules();
109113
}
110114
return moduleCache;
111115
}
116+
117+
interface PackageJson {
118+
dependencies?: Record<string, string>;
119+
devDependencies?: Record<string, string>;
120+
}
121+
122+
function getPackageJson(): PackageJson {
123+
try {
124+
const filePath = join(process.cwd(), 'package.json');
125+
const packageJson = JSON.parse(readFileSync(filePath, 'utf8')) as PackageJson;
126+
127+
return packageJson;
128+
} catch (e) {
129+
return {};
130+
}
131+
}
132+
133+
function getModulesFromPackageJson(): ModuleInfo {
134+
const packageJson = getPackageJson();
135+
136+
return {
137+
...packageJson.dependencies,
138+
...packageJson.devDependencies,
139+
};
140+
}

packages/node/src/sdk/index.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ import { defaultStackParser, getSentryRelease } from './api';
4040
import { NodeClient } from './client';
4141
import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel';
4242

43-
function getCjsOnlyIntegrations(): Integration[] {
44-
return isCjs() ? [modulesIntegration()] : [];
45-
}
46-
4743
/**
4844
* Get default integrations, excluding performance.
4945
*/
@@ -69,7 +65,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] {
6965
nodeContextIntegration(),
7066
childProcessIntegration(),
7167
processSessionIntegration(),
72-
...getCjsOnlyIntegrations(),
68+
modulesIntegration(),
7369
];
7470
}
7571

0 commit comments

Comments
 (0)