From 6c95934493af574f54c73c70233cecf13ed46b49 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 11 Jun 2025 16:30:55 +0200 Subject: [PATCH 1/2] feat(nextjs): Ensure all packages we use are externalized So they can be instrumented by us. --- packages/nextjs/src/config/types.ts | 1 + .../nextjs/src/config/withSentryConfig.ts | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 965233d08b76..5498e0b823a2 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -49,6 +49,7 @@ export type NextConfigObject = { productionBrowserSourceMaps?: boolean; // https://nextjs.org/docs/pages/api-reference/next-config-js/env env?: Record; + serverExternalPackages?: string[]; }; export type SentryBuildOptions = { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 543f271c1999..e630035c61a4 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -17,6 +17,35 @@ import { constructWebpackConfigFunction } from './webpack'; let showedExportModeTunnelWarning = false; let showedExperimentalBuildModeWarning = false; +// Packages we auto-instrument need to be external for instrumentation to work +// Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages +// Others we need to add ourselves +const DEFAULT_SERVER_EXTERNAL_PACKAGES = [ + 'ai', + 'amqplib', + 'connect', + 'dataloader', + 'express', + 'generic-pool', + 'graphql', + '@hapi/hapi', + 'ioredis', + 'kafkajs', + 'koa', + 'lru-memoizer', + 'mongodb', + 'mongoose', + 'mysql', + 'mysql2', + 'knex', + 'pg', + 'pg-pool', + '@node-redis/client', + '@redis/client', + 'redis', + 'tedious', +]; + /** * Modifies the passed in Next.js configuration with automatic build-time instrumentation and source map upload. * @@ -229,6 +258,10 @@ function getFinalConfigObject( return { ...incomingUserNextConfigObject, + serverExternalPackages: [ + ...(incomingUserNextConfigObject.serverExternalPackages || []), + ...DEFAULT_SERVER_EXTERNAL_PACKAGES, + ], webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName), }; } From dd6e6bdc91344e6da2128c8e258d819f08884746 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 16 Jun 2025 10:14:13 +0200 Subject: [PATCH 2/2] respect nextjs versions for setting defaults --- packages/nextjs/src/config/types.ts | 3 +- .../nextjs/src/config/withSentryConfig.ts | 24 ++++-- .../test/config/withSentryConfig.test.ts | 83 +++++++++++++++++-- 3 files changed, 97 insertions(+), 13 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 5498e0b823a2..ffce8a8b0641 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -45,11 +45,12 @@ export type NextConfigObject = { experimental?: { instrumentationHook?: boolean; clientTraceMetadata?: string[]; + serverComponentsExternalPackages?: string[]; // next < v15.0.0 }; productionBrowserSourceMaps?: boolean; // https://nextjs.org/docs/pages/api-reference/next-config-js/env env?: Record; - serverExternalPackages?: string[]; + serverExternalPackages?: string[]; // next >= v15.0.0 }; export type SentryBuildOptions = { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index e630035c61a4..b94ce6187f97 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -20,7 +20,7 @@ let showedExperimentalBuildModeWarning = false; // Packages we auto-instrument need to be external for instrumentation to work // Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages // Others we need to add ourselves -const DEFAULT_SERVER_EXTERNAL_PACKAGES = [ +export const DEFAULT_SERVER_EXTERNAL_PACKAGES = [ 'ai', 'amqplib', 'connect', @@ -219,8 +219,10 @@ function getFinalConfigObject( ); } + let nextMajor: number | undefined; if (nextJsVersion) { const { major, minor, patch, prerelease } = parseSemver(nextJsVersion); + nextMajor = major; const isSupportedVersion = major !== undefined && minor !== undefined && @@ -258,10 +260,22 @@ function getFinalConfigObject( return { ...incomingUserNextConfigObject, - serverExternalPackages: [ - ...(incomingUserNextConfigObject.serverExternalPackages || []), - ...DEFAULT_SERVER_EXTERNAL_PACKAGES, - ], + ...(nextMajor && nextMajor >= 15 + ? { + serverExternalPackages: [ + ...(incomingUserNextConfigObject.serverExternalPackages || []), + ...DEFAULT_SERVER_EXTERNAL_PACKAGES, + ], + } + : { + experimental: { + ...incomingUserNextConfigObject.experimental, + serverComponentsExternalPackages: [ + ...(incomingUserNextConfigObject.experimental?.serverComponentsExternalPackages || []), + ...DEFAULT_SERVER_EXTERNAL_PACKAGES, + ], + }, + }), webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName), }; } diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index f9db1a68771e..ee4b2d364c6a 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -1,4 +1,6 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as util from '../../src/config/util'; +import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from '../../src/config/withSentryConfig'; import { defaultRuntimePhase, defaultsObject, exportedNextConfig, userNextConfig } from './fixtures'; import { materializeFinalNextConfig } from './testUtils'; @@ -22,10 +24,16 @@ describe('withSentryConfig', () => { it("works when user's overall config is an object", () => { const finalConfig = materializeFinalNextConfig(exportedNextConfig); - expect(finalConfig).toEqual( + const { webpack, experimental, ...restOfFinalConfig } = finalConfig; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { webpack: _userWebpack, experimental: _userExperimental, ...restOfUserConfig } = userNextConfig; + + expect(restOfFinalConfig).toEqual(restOfUserConfig); + expect(webpack).toBeInstanceOf(Function); + expect(experimental).toEqual( expect.objectContaining({ - ...userNextConfig, - webpack: expect.any(Function), // `webpack` is tested specifically elsewhere + instrumentationHook: true, + serverComponentsExternalPackages: expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES), }), ); }); @@ -35,10 +43,21 @@ describe('withSentryConfig', () => { const finalConfig = materializeFinalNextConfig(exportedNextConfigFunction); - expect(finalConfig).toEqual( + const { webpack, experimental, ...restOfFinalConfig } = finalConfig; + const { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + webpack: _userWebpack, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + experimental: _userExperimental, + ...restOfUserConfig + } = exportedNextConfigFunction(); + + expect(restOfFinalConfig).toEqual(restOfUserConfig); + expect(webpack).toBeInstanceOf(Function); + expect(experimental).toEqual( expect.objectContaining({ - ...exportedNextConfigFunction(), - webpack: expect.any(Function), // `webpack` is tested specifically elsewhere + instrumentationHook: true, + serverComponentsExternalPackages: expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES), }), ); }); @@ -75,4 +94,54 @@ describe('withSentryConfig', () => { consoleWarnSpy.mockRestore(); } }); + + describe('server packages configuration', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('uses serverExternalPackages for Next.js 15+', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.serverExternalPackages).toBeDefined(); + expect(finalConfig.serverExternalPackages).toEqual(expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES)); + expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeUndefined(); + }); + + it('uses experimental.serverComponentsExternalPackages for Next.js < 15', () => { + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0'); + const finalConfig = materializeFinalNextConfig(exportedNextConfig); + + expect(finalConfig.serverExternalPackages).toBeUndefined(); + expect(finalConfig.experimental?.serverComponentsExternalPackages).toBeDefined(); + expect(finalConfig.experimental?.serverComponentsExternalPackages).toEqual( + expect.arrayContaining(DEFAULT_SERVER_EXTERNAL_PACKAGES), + ); + }); + + it('preserves existing packages in both versions', () => { + const existingPackages = ['@some/existing-package']; + + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.0.0'); + const config15 = materializeFinalNextConfig({ + ...exportedNextConfig, + serverExternalPackages: existingPackages, + }); + expect(config15.serverExternalPackages).toEqual( + expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]), + ); + + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('14.0.0'); + const config14 = materializeFinalNextConfig({ + ...exportedNextConfig, + experimental: { + serverComponentsExternalPackages: existingPackages, + }, + }); + expect(config14.experimental?.serverComponentsExternalPackages).toEqual( + expect.arrayContaining([...existingPackages, ...DEFAULT_SERVER_EXTERNAL_PACKAGES]), + ); + }); + }); });