Skip to content

Commit 550288e

Browse files
authored
feat(nextjs)!: Don't rely on Next.js Build ID for release names (#14939)
Resolves #14940
1 parent fc6d51c commit 550288e

File tree

6 files changed

+108
-58
lines changed

6 files changed

+108
-58
lines changed

docs/migration/v8-to-v9.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ In v9, an `undefined` value will be treated the same as if the value is not defi
9292

9393
- The `captureUserFeedback` method has been removed. Use `captureFeedback` instead and update the `comments` field to `message`.
9494

95+
### `@sentry/nextjs`
96+
97+
- The Sentry Next.js SDK will no longer use the Next.js Build ID as fallback identifier for releases. The SDK will continue to attempt to read CI-provider-specific environment variables and the current git SHA to automatically determine a release name. If you examine that you no longer see releases created in Sentry, it is recommended to manually provide a release name to `withSentryConfig` via the `release.name` option.
98+
99+
This behavior was changed because the Next.js Build ID is non-deterministic and the release name is injected into client bundles, causing build artifacts to be non-deterministic. This caused issues for some users. Additionally, because it is uncertain whether it will be possible to rely on a Build ID when Turbopack becomes stable, we decided to pull the plug now instead of introducing confusing behavior in the future.
100+
95101
### Uncategorized (TODO)
96102

97103
TODO

packages/nextjs/src/config/webpack.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import * as fs from 'fs';
55
import * as path from 'path';
66
import { escapeStringForRegex, loadModule, logger } from '@sentry/core';
7-
import { getSentryRelease } from '@sentry/node';
87
import * as chalk from 'chalk';
98
import { sync as resolveSync } from 'resolve';
109

@@ -43,6 +42,7 @@ let showedMissingGlobalErrorWarningMsg = false;
4342
export function constructWebpackConfigFunction(
4443
userNextConfig: NextConfigObject = {},
4544
userSentryOptions: SentryBuildOptions = {},
45+
releaseName: string | undefined,
4646
): WebpackConfigFunction {
4747
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
4848
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
@@ -71,7 +71,7 @@ export function constructWebpackConfigFunction(
7171
const newConfig = setUpModuleRules(rawNewConfig);
7272

7373
// Add a loader which will inject code that sets global values
74-
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext);
74+
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, releaseName);
7575

7676
addOtelWarningIgnoreRule(newConfig);
7777

@@ -358,7 +358,7 @@ export function constructWebpackConfigFunction(
358358

359359
newConfig.plugins = newConfig.plugins || [];
360360
const sentryWebpackPluginInstance = sentryWebpackPlugin(
361-
getWebpackPluginOptions(buildContext, userSentryOptions),
361+
getWebpackPluginOptions(buildContext, userSentryOptions, releaseName),
362362
);
363363
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
364364
sentryWebpackPluginInstance._name = 'sentry-webpack-plugin'; // For tests and debugging. Serves no other purpose.
@@ -580,6 +580,7 @@ function addValueInjectionLoader(
580580
userNextConfig: NextConfigObject,
581581
userSentryOptions: SentryBuildOptions,
582582
buildContext: BuildContext,
583+
releaseName: string | undefined,
583584
): void {
584585
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
585586

@@ -592,9 +593,7 @@ function addValueInjectionLoader(
592593

593594
// The webpack plugin's release injection breaks the `app` directory so we inject the release manually here instead.
594595
// Having a release defined in dev-mode spams releases in Sentry so we only set one in non-dev mode
595-
SENTRY_RELEASE: buildContext.dev
596-
? undefined
597-
: { id: userSentryOptions.release?.name ?? getSentryRelease(buildContext.buildId) },
596+
SENTRY_RELEASE: releaseName && !buildContext.dev ? { id: releaseName } : undefined,
598597
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
599598
};
600599

packages/nextjs/src/config/webpackPluginOptions.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as path from 'path';
2-
import { getSentryRelease } from '@sentry/node';
32
import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin';
43
import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types';
54

@@ -10,8 +9,9 @@ import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types
109
export function getWebpackPluginOptions(
1110
buildContext: BuildContext,
1211
sentryBuildOptions: SentryBuildOptions,
12+
releaseName: string | undefined,
1313
): SentryWebpackPluginOptions {
14-
const { buildId, isServer, config: userNextConfig, dir, nextRuntime } = buildContext;
14+
const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext;
1515

1616
const prefixInsert = !isServer ? 'Client' : nextRuntime === 'edge' ? 'Edge' : 'Node.js';
1717

@@ -92,17 +92,24 @@ export function getWebpackPluginOptions(
9292
: undefined,
9393
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
9494
},
95-
release: {
96-
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
97-
name: sentryBuildOptions.release?.name ?? getSentryRelease(buildId),
98-
create: sentryBuildOptions.release?.create,
99-
finalize: sentryBuildOptions.release?.finalize,
100-
dist: sentryBuildOptions.release?.dist,
101-
vcsRemote: sentryBuildOptions.release?.vcsRemote,
102-
setCommits: sentryBuildOptions.release?.setCommits,
103-
deploy: sentryBuildOptions.release?.deploy,
104-
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
105-
},
95+
release:
96+
releaseName !== undefined
97+
? {
98+
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
99+
name: releaseName,
100+
create: sentryBuildOptions.release?.create,
101+
finalize: sentryBuildOptions.release?.finalize,
102+
dist: sentryBuildOptions.release?.dist,
103+
vcsRemote: sentryBuildOptions.release?.vcsRemote,
104+
setCommits: sentryBuildOptions.release?.setCommits,
105+
deploy: sentryBuildOptions.release?.deploy,
106+
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
107+
}
108+
: {
109+
inject: false,
110+
create: false,
111+
finalize: false,
112+
},
106113
bundleSizeOptimizations: {
107114
...sentryBuildOptions.bundleSizeOptimizations,
108115
},

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* eslint-disable complexity */
22
import { isThenable, parseSemver } from '@sentry/core';
33

4+
import * as childProcess from 'child_process';
45
import * as fs from 'fs';
6+
import { getSentryRelease } from '@sentry/node';
57
import { sync as resolveSync } from 'resolve';
68
import type {
79
ExportedNextConfig as NextConfig,
@@ -20,7 +22,6 @@ let showedExportModeTunnelWarning = false;
2022
* @param sentryBuildOptions Additional options to configure instrumentation and
2123
* @returns The modified config to be exported
2224
*/
23-
// TODO(v9): Always return an async function here to allow us to do async things like grabbing a deterministic build ID.
2425
export function withSentryConfig<C>(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C {
2526
const castNextConfig = (nextConfig as NextConfig) || {};
2627
if (typeof castNextConfig === 'function') {
@@ -174,9 +175,11 @@ function getFinalConfigObject(
174175
);
175176
}
176177

178+
const releaseName = userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision();
179+
177180
return {
178181
...incomingUserNextConfigObject,
179-
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions),
182+
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName),
180183
};
181184
}
182185

@@ -316,3 +319,16 @@ function resolveNextjsPackageJson(): string | undefined {
316319
return undefined;
317320
}
318321
}
322+
323+
function getGitRevision(): string | undefined {
324+
let gitRevision: string | undefined;
325+
try {
326+
gitRevision = childProcess
327+
.execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
328+
.toString()
329+
.trim();
330+
} catch (e) {
331+
// noop
332+
}
333+
return gitRevision;
334+
}

packages/nextjs/test/config/testUtils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function materializeFinalWebpackConfig(options: {
6969
const webpackConfigFunction = constructWebpackConfigFunction(
7070
materializedUserNextConfig,
7171
options.sentryBuildTimeOptions,
72+
undefined,
7273
);
7374

7475
// call it to get concrete values for comparison

packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,40 @@ function generateBuildContext(overrides: {
2424
describe('getWebpackPluginOptions()', () => {
2525
it('forwards relevant options', () => {
2626
const buildContext = generateBuildContext({ isServer: false });
27-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {
28-
authToken: 'my-auth-token',
29-
headers: { 'my-test-header': 'test' },
30-
org: 'my-org',
31-
project: 'my-project',
32-
telemetry: false,
33-
reactComponentAnnotation: {
34-
enabled: true,
35-
},
36-
silent: false,
37-
debug: true,
38-
sentryUrl: 'my-url',
39-
sourcemaps: {
40-
assets: ['my-asset'],
41-
ignore: ['my-ignore'],
42-
},
43-
release: {
44-
name: 'my-release',
45-
create: false,
46-
finalize: false,
47-
dist: 'my-dist',
48-
vcsRemote: 'my-origin',
49-
setCommits: {
50-
auto: true,
27+
const generatedPluginOptions = getWebpackPluginOptions(
28+
buildContext,
29+
{
30+
authToken: 'my-auth-token',
31+
headers: { 'my-test-header': 'test' },
32+
org: 'my-org',
33+
project: 'my-project',
34+
telemetry: false,
35+
reactComponentAnnotation: {
36+
enabled: true,
5137
},
52-
deploy: {
53-
env: 'my-env',
38+
silent: false,
39+
debug: true,
40+
sentryUrl: 'my-url',
41+
sourcemaps: {
42+
assets: ['my-asset'],
43+
ignore: ['my-ignore'],
44+
},
45+
release: {
46+
name: 'my-release',
47+
create: false,
48+
finalize: false,
49+
dist: 'my-dist',
50+
vcsRemote: 'my-origin',
51+
setCommits: {
52+
auto: true,
53+
},
54+
deploy: {
55+
env: 'my-env',
56+
},
5457
},
5558
},
56-
});
59+
'my-release',
60+
);
5761

5862
expect(generatedPluginOptions.authToken).toBe('my-auth-token');
5963
expect(generatedPluginOptions.debug).toBe(true);
@@ -111,12 +115,16 @@ describe('getWebpackPluginOptions()', () => {
111115

112116
it('forwards bundleSizeOptimization options', () => {
113117
const buildContext = generateBuildContext({ isServer: false });
114-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {
115-
bundleSizeOptimizations: {
116-
excludeTracing: true,
117-
excludeReplayShadowDom: false,
118+
const generatedPluginOptions = getWebpackPluginOptions(
119+
buildContext,
120+
{
121+
bundleSizeOptimizations: {
122+
excludeTracing: true,
123+
excludeReplayShadowDom: false,
124+
},
118125
},
119-
});
126+
undefined,
127+
);
120128

121129
expect(generatedPluginOptions).toMatchObject({
122130
bundleSizeOptimizations: {
@@ -128,7 +136,7 @@ describe('getWebpackPluginOptions()', () => {
128136

129137
it('returns the right `assets` and `ignore` values during the server build', () => {
130138
const buildContext = generateBuildContext({ isServer: true });
131-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {});
139+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined);
132140
expect(generatedPluginOptions.sourcemaps).toMatchObject({
133141
assets: ['/my/project/dir/.next/server/**', '/my/project/dir/.next/serverless/**'],
134142
ignore: [],
@@ -137,7 +145,7 @@ describe('getWebpackPluginOptions()', () => {
137145

138146
it('returns the right `assets` and `ignore` values during the client build', () => {
139147
const buildContext = generateBuildContext({ isServer: false });
140-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {});
148+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined);
141149
expect(generatedPluginOptions.sourcemaps).toMatchObject({
142150
assets: ['/my/project/dir/.next/static/chunks/pages/**', '/my/project/dir/.next/static/chunks/app/**'],
143151
ignore: [
@@ -152,7 +160,7 @@ describe('getWebpackPluginOptions()', () => {
152160

153161
it('returns the right `assets` and `ignore` values during the client build with `widenClientFileUpload`', () => {
154162
const buildContext = generateBuildContext({ isServer: false });
155-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true });
163+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined);
156164
expect(generatedPluginOptions.sourcemaps).toMatchObject({
157165
assets: ['/my/project/dir/.next/static/chunks/**'],
158166
ignore: [
@@ -167,7 +175,7 @@ describe('getWebpackPluginOptions()', () => {
167175

168176
it('sets `sourcemaps.assets` to an empty array when `sourcemaps.disable` is true', () => {
169177
const buildContext = generateBuildContext({ isServer: false });
170-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } });
178+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } }, undefined);
171179
expect(generatedPluginOptions.sourcemaps).toMatchObject({
172180
assets: [],
173181
});
@@ -179,7 +187,7 @@ describe('getWebpackPluginOptions()', () => {
179187
nextjsConfig: { distDir: '.dist\\v1' },
180188
isServer: false,
181189
});
182-
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true });
190+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined);
183191
expect(generatedPluginOptions.sourcemaps).toMatchObject({
184192
assets: ['C:/my/windows/project/dir/.dist/v1/static/chunks/**'],
185193
ignore: [
@@ -191,4 +199,17 @@ describe('getWebpackPluginOptions()', () => {
191199
],
192200
});
193201
});
202+
203+
it('sets options to not create a release or do any release operations when releaseName is undefined', () => {
204+
const buildContext = generateBuildContext({ isServer: false });
205+
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined);
206+
207+
expect(generatedPluginOptions).toMatchObject({
208+
release: {
209+
inject: false,
210+
create: false,
211+
finalize: false,
212+
},
213+
});
214+
});
194215
});

0 commit comments

Comments
 (0)