Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,14 @@
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/semantic-conventions": "^1.34.0",
"@rollup/plugin-commonjs": "28.0.1",
"@sentry/bundler-plugin-core": "^4.3.0",
"@sentry/webpack-plugin": "^4.3.0",
"@sentry-internal/browser-utils": "10.9.0",
"@sentry/core": "10.9.0",
"@sentry/node": "10.9.0",
"@sentry/opentelemetry": "10.9.0",
"@sentry/react": "10.9.0",
"@sentry/vercel-edge": "10.9.0",
"@sentry/webpack-plugin": "^4.1.1",
"chalk": "3.0.0",
"resolve": "1.22.8",
"rollup": "^4.35.0",
Expand Down
97 changes: 97 additions & 0 deletions packages/nextjs/src/config/getBuildPluginOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugin-core';
import * as path from 'path';
import type { SentryBuildOptions } from './types';

/**
* Get Sentry Build Plugin options for the runAfterProductionCompile hook.
*/
export function getBuildPluginOptions({
sentryBuildOptions,
releaseName,
distDirAbsPath,
}: {
sentryBuildOptions: SentryBuildOptions;
releaseName: string | undefined;
distDirAbsPath: string;
}): SentryBuildPluginOptions {
const sourcemapUploadAssets: string[] = [];
const sourcemapUploadIgnore: string[] = [];

const filesToDeleteAfterUpload: string[] = [];

// We need to convert paths to posix because Glob patterns use `\` to escape
// glob characters. This clashes with Windows path separators.
// See: https://www.npmjs.com/package/glob
const normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/');

sourcemapUploadAssets.push(
path.posix.join(normalizedDistDirAbsPath, '**'), // Next.js build output
);
if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) {
filesToDeleteAfterUpload.push(
path.posix.join(normalizedDistDirAbsPath, '**', '*.js.map'),
path.posix.join(normalizedDistDirAbsPath, '**', '*.mjs.map'),
path.posix.join(normalizedDistDirAbsPath, '**', '*.cjs.map'),
);
}

return {
authToken: sentryBuildOptions.authToken,
headers: sentryBuildOptions.headers,
org: sentryBuildOptions.org,
project: sentryBuildOptions.project,
telemetry: sentryBuildOptions.telemetry,
debug: sentryBuildOptions.debug,
errorHandler: sentryBuildOptions.errorHandler,
reactComponentAnnotation: {
...sentryBuildOptions.reactComponentAnnotation,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation,
},
silent: sentryBuildOptions.silent,
url: sentryBuildOptions.sentryUrl,
sourcemaps: {
disable: sentryBuildOptions.sourcemaps?.disable,
rewriteSources(source) {
if (source.startsWith('webpack://_N_E/')) {
return source.replace('webpack://_N_E/', '');
} else if (source.startsWith('webpack://')) {
return source.replace('webpack://', '');
} else {
return source;
}
},
assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets,
ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore,
filesToDeleteAfterUpload,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
},
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,
},
_metaOptions: {
loggerPrefixOverride: '[@sentry/nextjs]',
telemetry: {
metaFramework: 'nextjs',
},
},
...sentryBuildOptions.unstable_sentryWebpackPluginOptions,
};
}
59 changes: 59 additions & 0 deletions packages/nextjs/src/config/handleRunAfterProductionCompile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core';
import { loadModule } from '@sentry/core';
import { getBuildPluginOptions } from './getBuildPluginOptions';
import type { SentryBuildOptions } from './types';

/**
* This function is called by Next.js after the production build is complete.
* It is used to upload sourcemaps to Sentry.
*/
export async function handleRunAfterProductionCompile(
{ releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' },
sentryBuildOptions: SentryBuildOptions,
): Promise<void> {
// We don't want to do anything for webpack at this point because the plugin already handles this
// TODO: Actually implement this for webpack as well
if (buildTool === 'webpack') {
return;
}

if (sentryBuildOptions.debug) {
// eslint-disable-next-line no-console
console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.');
}

const { createSentryBuildPluginManager } =
loadModule<{ createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType }>(
'@sentry/bundler-plugin-core',
module,
) ?? {};

if (!createSentryBuildPluginManager) {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.',
);
return;
}

const sentryBuildPluginManager = createSentryBuildPluginManager(
getBuildPluginOptions({
sentryBuildOptions,
releaseName,
distDirAbsPath: distDir,
}),
{
buildTool,
loggerPrefix: '[@sentry/nextjs]',
},
);

await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
await sentryBuildPluginManager.createRelease();
await sentryBuildPluginManager.injectDebugIds([distDir]);
await sentryBuildPluginManager.uploadSourcemaps([distDir], {
// We don't want to prepare the artifacts because we injected debug ids manually before
prepareArtifacts: false,
});
await sentryBuildPluginManager.deleteArtifacts();
}
12 changes: 12 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ export type NextConfigObject = {
env?: Record<string, string>;
serverExternalPackages?: string[]; // next >= v15.0.0
turbopack?: TurbopackOptions;
compiler?: {
runAfterProductionCompile?: (context: { distDir: string; projectDir: string }) => Promise<void> | void;
};
};

export type SentryBuildOptions = {
Expand Down Expand Up @@ -504,6 +507,15 @@ export type SentryBuildOptions = {
* Use with caution in production environments.
*/
_experimental?: Partial<{
/**
* When true (and Next.js >= 15), use the runAfterProductionCompile hook to consolidate sourcemap uploads
* into a single operation after turbopack builds complete, reducing build time.
*
* When false, use the traditional approach of uploading sourcemaps during each webpack build.
*
* @default false
*/
useRunAfterProductionCompileHook?: boolean;
thirdPartyOriginStackFrames: boolean;
}>;
};
Expand Down
37 changes: 37 additions & 0 deletions packages/nextjs/src/config/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseSemver } from '@sentry/core';
import * as fs from 'fs';
import { sync as resolveSync } from 'resolve';

Expand Down Expand Up @@ -27,3 +28,39 @@ function resolveNextjsPackageJson(): string | undefined {
return undefined;
}
}

/**
* Checks if the current Next.js version supports the runAfterProductionCompile hook.
* This hook was introduced in Next.js 15.4.1. (https://github.com/vercel/next.js/pull/77345)
*
* @returns true if Next.js version is 15.4.1 or higher
*/
export function supportsProductionCompileHook(): boolean {
const version = getNextjsVersion();
if (!version) {
return false;
}

const { major, minor, patch } = parseSemver(version);

if (major === undefined || minor === undefined || patch === undefined) {
return false;
}

if (major > 15) {
return true;
}

// For major version 15, check if it's 15.4.1 or higher
if (major === 15) {
if (minor > 4) {
return true;
}
if (minor === 4 && patch >= 1) {
return true;
}
return false;
}

return false;
}
56 changes: 55 additions & 1 deletion packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSentryRelease } from '@sentry/node';
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { handleRunAfterProductionCompile } from './handleRunAfterProductionCompile';
import { createRouteManifest } from './manifest/createRouteManifest';
import type { RouteManifest } from './manifest/types';
import { constructTurbopackConfig } from './turbopack';
Expand All @@ -14,7 +15,7 @@ import type {
NextConfigObject,
SentryBuildOptions,
} from './types';
import { getNextjsVersion } from './util';
import { getNextjsVersion, supportsProductionCompileHook } from './util';
import { constructWebpackConfigFunction } from './webpack';

let showedExportModeTunnelWarning = false;
Expand Down Expand Up @@ -293,6 +294,59 @@ function getFinalConfigObject(
}
}

if (userSentryOptions?._experimental?.useRunAfterProductionCompileHook === true && supportsProductionCompileHook()) {
if (incomingUserNextConfigObject?.compiler?.runAfterProductionCompile === undefined) {
incomingUserNextConfigObject.compiler ??= {};
incomingUserNextConfigObject.compiler.runAfterProductionCompile = async ({ distDir }) => {
await handleRunAfterProductionCompile(
{ releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' },
userSentryOptions,
);
};
} else if (typeof incomingUserNextConfigObject.compiler.runAfterProductionCompile === 'function') {
incomingUserNextConfigObject.compiler.runAfterProductionCompile = new Proxy(
incomingUserNextConfigObject.compiler.runAfterProductionCompile,
{
async apply(target, thisArg, argArray) {
const { distDir }: { distDir: string } = argArray[0] ?? { distDir: '.next' };
await target.apply(thisArg, argArray);
await handleRunAfterProductionCompile(
{ releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' },
userSentryOptions,
);
},
},
);
} else {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] The configured `compiler.runAfterProductionCompile` option is not a function. Will not run source map and release management logic.',
);
}
}

// Enable source maps for turbopack builds
if (isTurbopackSupported && isTurbopack && !userSentryOptions.sourcemaps?.disable) {
// Only set if not already configured by user
if (incomingUserNextConfigObject.productionBrowserSourceMaps === undefined) {
// eslint-disable-next-line no-console
console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.');
incomingUserNextConfigObject.productionBrowserSourceMaps = true;

// Enable source map deletion if not explicitly disabled
if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] Source maps will be automatically deleted after being uploaded to Sentry. If you want to keep the source maps, set the `sourcemaps.deleteSourcemapsAfterUpload` option to false in `withSentryConfig()`. If you do not want to generate and upload sourcemaps at all, set the `sourcemaps.disable` option to true.',
);
userSentryOptions.sourcemaps = {
...userSentryOptions.sourcemaps,
deleteSourcemapsAfterUpload: true,
};
}
}
}

return {
...incomingUserNextConfigObject,
...(nextMajor && nextMajor >= 15
Expand Down
Loading
Loading