diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 965233d08b76..ffce8a8b0641 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -45,10 +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[]; // next >= v15.0.0 }; export type SentryBuildOptions = { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 543f271c1999..b94ce6187f97 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 +export 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. * @@ -190,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 && @@ -229,6 +260,22 @@ function getFinalConfigObject( return { ...incomingUserNextConfigObject, + ...(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]), + ); + }); + }); }); 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