diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 498c9b969ed9..8208cd603c98 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -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), + }), + ); }); diff --git a/dev-packages/node-integration-tests/suites/modules/instrument.mjs b/dev-packages/node-integration-tests/suites/modules/instrument.mjs new file mode 100644 index 000000000000..9ffde125d498 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js new file mode 100644 index 000000000000..9b24c0845ac0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -0,0 +1,22 @@ +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, +}); + +// 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); diff --git a/dev-packages/node-integration-tests/suites/modules/server.mjs b/dev-packages/node-integration-tests/suites/modules/server.mjs new file mode 100644 index 000000000000..6edeb78c703f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/server.mjs @@ -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); diff --git a/dev-packages/node-integration-tests/suites/modules/test.ts b/dev-packages/node-integration-tests/suites/modules/test.ts new file mode 100644 index 000000000000..89fe98c62867 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/test.ts @@ -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(); + }); +}); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 322f2e320624..8898b3495ba9 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -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; }; } @@ -825,3 +833,21 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules) newConfig.ignoreWarnings.push(...ignoreRules); } } + +function _getModules(projectDir: string): Record { + try { + const packageJson = path.join(projectDir, 'package.json'); + const packageJsonContent = fs.readFileSync(packageJson, 'utf8'); + const packageJsonObject = JSON.parse(packageJsonContent) as { + dependencies?: Record; + devDependencies?: Record; + }; + + return { + ...packageJsonObject.dependencies, + ...packageJsonObject.devDependencies, + }; + } catch { + return {}; + } +} diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index e15aa9dd245b..50f3a3b3aa8d 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -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; + +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; + +/** + * `__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) { @@ -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) : []; } catch (e) { @@ -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(); paths.forEach(path => { let dir = path; @@ -71,7 +76,7 @@ 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) { @@ -79,7 +84,7 @@ function collectModules(): { } const pkgfile = join(orig, 'package.json'); - seen[orig] = true; + seen.add(orig); if (!existsSync(pkgfile)) { return updir(); @@ -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; + devDependencies?: Record; +} + +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, + }; +} diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 1536242cfdcb..e693d3976fe4 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -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. */ @@ -69,7 +65,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { nodeContextIntegration(), childProcessIntegration(), processSessionIntegration(), - ...getCjsOnlyIntegrations(), + modulesIntegration(), ]; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy