From 06ed9c4f35bd068199eee8d595cbdae4d22ccbea Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 23 Jun 2025 18:43:11 +0200 Subject: [PATCH] stop injecting release value when create release is set to false --- packages/nextjs/src/config/webpack.ts | 7 +- .../nextjs/src/config/withSentryConfig.ts | 11 ++- .../test/config/withSentryConfig.test.ts | 67 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index aeef52d4e309..bcfbbd2e151f 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -689,6 +689,10 @@ function addValueInjectionLoader( ): void { const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || ''; + // Check if release creation is disabled to prevent injection that breaks build determinism + const shouldCreateRelease = userSentryOptions.release?.create !== false; + const releaseToInject = releaseName && shouldCreateRelease ? releaseName : undefined; + const isomorphicValues = { // `rewritesTunnel` set by the user in Next.js config _sentryRewritesTunnelPath: @@ -700,7 +704,8 @@ function addValueInjectionLoader( // The webpack plugin's release injection breaks the `app` directory so we inject the release manually here instead. // Having a release defined in dev-mode spams releases in Sentry so we only set one in non-dev mode - SENTRY_RELEASE: releaseName && !buildContext.dev ? { id: releaseName } : undefined, + // Only inject if release creation is not explicitly disabled (to maintain build determinism) + SENTRY_RELEASE: releaseToInject && !buildContext.dev ? { id: releaseToInject } : undefined, _sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined, }; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index ac1e76231eb8..c8eabd00d85c 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -90,7 +90,12 @@ function getFinalConfigObject( incomingUserNextConfigObject: NextConfigObject, userSentryOptions: SentryBuildOptions, ): NextConfigObject { - const releaseName = userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision(); + // Only determine a release name if release creation is not explicitly disabled + // This prevents injection of Git commit hashes that break build determinism + const shouldCreateRelease = userSentryOptions.release?.create !== false; + const releaseName = shouldCreateRelease + ? userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision() + : userSentryOptions.release?.name; if (userSentryOptions?.tunnelRoute) { if (incomingUserNextConfigObject.output === 'export') { @@ -126,8 +131,8 @@ function getFinalConfigObject( // 1. compile: Code compilation // 2. generate: Environment variable inlining and prerendering (We don't instrument this phase, we inline in the compile phase) // - // We assume a single “full” build and reruns Webpack instrumentation in both phases. - // During the generate step it collides with Next.js’s inliner + // We assume a single "full" build and reruns Webpack instrumentation in both phases. + // During the generate step it collides with Next.js's inliner // producing malformed JS and build failures. // We skip Sentry processing during generate to avoid this issue. return incomingUserNextConfigObject; diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index ee4b2d364c6a..f1f1ae13f9fa 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -144,4 +144,71 @@ describe('withSentryConfig', () => { ); }); }); + + describe('release injection behavior', () => { + afterEach(() => { + vi.restoreAllMocks(); + + // clear env to avoid leaking env vars from fixtures + delete exportedNextConfig.env; + delete process.env.SENTRY_RELEASE; + }); + + it('does not inject release when create is false', () => { + const sentryOptions = { + release: { + create: false, + }, + }; + + // clear env to avoid leaking env vars from fixtures + delete exportedNextConfig.env; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should not inject release into environment when create is false + expect(finalConfig.env).not.toHaveProperty('_sentryRelease'); + }); + + it('injects release when create is true (default)', () => { + const sentryOptions = { + release: { + create: true, + name: 'test-release@1.0.0', + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should inject release into environment when create is true + expect(finalConfig.env).toHaveProperty('_sentryRelease', 'test-release@1.0.0'); + }); + + it('injects release with explicit name', () => { + const sentryOptions = { + release: { + name: 'custom-release-v2.1.0', + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + // Should inject the explicit release name + expect(finalConfig.env).toHaveProperty('_sentryRelease', 'custom-release-v2.1.0'); + }); + + it('falls back to SENTRY_RELEASE environment variable when no explicit name provided', () => { + process.env.SENTRY_RELEASE = 'env-release-1.5.0'; + + const sentryOptions = { + release: { + create: true, + }, + }; + + const finalConfig = materializeFinalNextConfig(exportedNextConfig, undefined, sentryOptions); + + expect(finalConfig.env).toHaveProperty('_sentryRelease', 'env-release-1.5.0'); + }); + }); });