Skip to content

Commit

Permalink
feat(nextjs)!: Don't rely on Next.js Build ID for release names (#14939)
Browse files Browse the repository at this point in the history
Resolves #14940
  • Loading branch information
lforst authored Jan 9, 2025
1 parent fc6d51c commit 550288e
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 58 deletions.
6 changes: 6 additions & 0 deletions docs/migration/v8-to-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ In v9, an `undefined` value will be treated the same as if the value is not defi

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

### `@sentry/nextjs`

- 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.

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.

### Uncategorized (TODO)

TODO
Expand Down
11 changes: 5 additions & 6 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { escapeStringForRegex, loadModule, logger } from '@sentry/core';
import { getSentryRelease } from '@sentry/node';
import * as chalk from 'chalk';
import { sync as resolveSync } from 'resolve';

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

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

addOtelWarningIgnoreRule(newConfig);

Expand Down Expand Up @@ -358,7 +358,7 @@ export function constructWebpackConfigFunction(

newConfig.plugins = newConfig.plugins || [];
const sentryWebpackPluginInstance = sentryWebpackPlugin(
getWebpackPluginOptions(buildContext, userSentryOptions),
getWebpackPluginOptions(buildContext, userSentryOptions, releaseName),
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
sentryWebpackPluginInstance._name = 'sentry-webpack-plugin'; // For tests and debugging. Serves no other purpose.
Expand Down Expand Up @@ -580,6 +580,7 @@ function addValueInjectionLoader(
userNextConfig: NextConfigObject,
userSentryOptions: SentryBuildOptions,
buildContext: BuildContext,
releaseName: string | undefined,
): void {
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';

Expand All @@ -592,9 +593,7 @@ 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: buildContext.dev
? undefined
: { id: userSentryOptions.release?.name ?? getSentryRelease(buildContext.buildId) },
SENTRY_RELEASE: releaseName && !buildContext.dev ? { id: releaseName } : undefined,
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
};

Expand Down
33 changes: 20 additions & 13 deletions packages/nextjs/src/config/webpackPluginOptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as path from 'path';
import { getSentryRelease } from '@sentry/node';
import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin';
import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types';

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

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

Expand Down Expand Up @@ -92,17 +92,24 @@ export function getWebpackPluginOptions(
: undefined,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
},
release: {
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
name: sentryBuildOptions.release?.name ?? getSentryRelease(buildId),
create: sentryBuildOptions.release?.create,
finalize: sentryBuildOptions.release?.finalize,
dist: sentryBuildOptions.release?.dist,
vcsRemote: sentryBuildOptions.release?.vcsRemote,
setCommits: sentryBuildOptions.release?.setCommits,
deploy: sentryBuildOptions.release?.deploy,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
},
release:
releaseName !== undefined
? {
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
name: releaseName,
create: sentryBuildOptions.release?.create,
finalize: sentryBuildOptions.release?.finalize,
dist: sentryBuildOptions.release?.dist,
vcsRemote: sentryBuildOptions.release?.vcsRemote,
setCommits: sentryBuildOptions.release?.setCommits,
deploy: sentryBuildOptions.release?.deploy,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
}
: {
inject: false,
create: false,
finalize: false,
},
bundleSizeOptimizations: {
...sentryBuildOptions.bundleSizeOptimizations,
},
Expand Down
20 changes: 18 additions & 2 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable complexity */
import { isThenable, parseSemver } from '@sentry/core';

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

const releaseName = userSentryOptions.release?.name ?? getSentryRelease() ?? getGitRevision();

return {
...incomingUserNextConfigObject,
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions),
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName),
};
}

Expand Down Expand Up @@ -316,3 +319,16 @@ function resolveNextjsPackageJson(): string | undefined {
return undefined;
}
}

function getGitRevision(): string | undefined {
let gitRevision: string | undefined;
try {
gitRevision = childProcess
.execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim();
} catch (e) {
// noop
}
return gitRevision;
}
1 change: 1 addition & 0 deletions packages/nextjs/test/config/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function materializeFinalWebpackConfig(options: {
const webpackConfigFunction = constructWebpackConfigFunction(
materializedUserNextConfig,
options.sentryBuildTimeOptions,
undefined,
);

// call it to get concrete values for comparison
Expand Down
95 changes: 58 additions & 37 deletions packages/nextjs/test/config/webpack/webpackPluginOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,40 @@ function generateBuildContext(overrides: {
describe('getWebpackPluginOptions()', () => {
it('forwards relevant options', () => {
const buildContext = generateBuildContext({ isServer: false });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {
authToken: 'my-auth-token',
headers: { 'my-test-header': 'test' },
org: 'my-org',
project: 'my-project',
telemetry: false,
reactComponentAnnotation: {
enabled: true,
},
silent: false,
debug: true,
sentryUrl: 'my-url',
sourcemaps: {
assets: ['my-asset'],
ignore: ['my-ignore'],
},
release: {
name: 'my-release',
create: false,
finalize: false,
dist: 'my-dist',
vcsRemote: 'my-origin',
setCommits: {
auto: true,
const generatedPluginOptions = getWebpackPluginOptions(
buildContext,
{
authToken: 'my-auth-token',
headers: { 'my-test-header': 'test' },
org: 'my-org',
project: 'my-project',
telemetry: false,
reactComponentAnnotation: {
enabled: true,
},
deploy: {
env: 'my-env',
silent: false,
debug: true,
sentryUrl: 'my-url',
sourcemaps: {
assets: ['my-asset'],
ignore: ['my-ignore'],
},
release: {
name: 'my-release',
create: false,
finalize: false,
dist: 'my-dist',
vcsRemote: 'my-origin',
setCommits: {
auto: true,
},
deploy: {
env: 'my-env',
},
},
},
});
'my-release',
);

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

it('forwards bundleSizeOptimization options', () => {
const buildContext = generateBuildContext({ isServer: false });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {
bundleSizeOptimizations: {
excludeTracing: true,
excludeReplayShadowDom: false,
const generatedPluginOptions = getWebpackPluginOptions(
buildContext,
{
bundleSizeOptimizations: {
excludeTracing: true,
excludeReplayShadowDom: false,
},
},
});
undefined,
);

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

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

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

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

it('sets `sourcemaps.assets` to an empty array when `sourcemaps.disable` is true', () => {
const buildContext = generateBuildContext({ isServer: false });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { sourcemaps: { disable: true } }, undefined);
expect(generatedPluginOptions.sourcemaps).toMatchObject({
assets: [],
});
Expand All @@ -179,7 +187,7 @@ describe('getWebpackPluginOptions()', () => {
nextjsConfig: { distDir: '.dist\\v1' },
isServer: false,
});
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, { widenClientFileUpload: true }, undefined);
expect(generatedPluginOptions.sourcemaps).toMatchObject({
assets: ['C:/my/windows/project/dir/.dist/v1/static/chunks/**'],
ignore: [
Expand All @@ -191,4 +199,17 @@ describe('getWebpackPluginOptions()', () => {
],
});
});

it('sets options to not create a release or do any release operations when releaseName is undefined', () => {
const buildContext = generateBuildContext({ isServer: false });
const generatedPluginOptions = getWebpackPluginOptions(buildContext, {}, undefined);

expect(generatedPluginOptions).toMatchObject({
release: {
inject: false,
create: false,
finalize: false,
},
});
});
});

0 comments on commit 550288e

Please sign in to comment.