From fb7b05ab72bd7847538d752174c1f165a52311b0 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 25 Aug 2026 09:41:47 +0200 Subject: [PATCH 1/6] feat(nextjs): Align build options with BuildTimeOptionsBase Build `SentryBuildOptions` on the shared `BuildTimeOptionsBase` instead of redeclaring the option set by hand. Next.js was the last meta-framework SDK not using the shared type, which is why #23372 had to manually add `moduleMetadata` and `sourcemaps.resolveSourceMap`. Six options stay declared locally, each for a Next.js-specific reason: `project` accepts `string[]`, `sourcemaps` adds `deleteSourcemapsAfterUpload`, `release` omits `inject` because the SDK always sets it, and `buildTimeInstrumentation` / `applicationKey` / `moduleMetadata` document webpack-vs-Turbopack behavior differences. Inheriting the base type widens `sourcemaps.disable` to accept `'disable-upload'`. Next.js checked the option for truthiness where it controls source map generation, which would have suppressed generation entirely rather than just the upload, so those checks now compare against `true` and the auto-delete defaults skip `'disable-upload'`. Co-Authored-By: Claude Opus 5 (1M context) --- packages/nextjs/src/config/types.ts | 409 +++--------------- packages/nextjs/src/config/webpack.ts | 11 +- .../getFinalConfigObjectBundlerUtils.ts | 10 +- .../nextjs/test/config/buildOptions.test-d.ts | 123 ++++++ .../webpack/constructWebpackConfig.test.ts | 34 ++ .../test/config/withSentryConfig.test.ts | 21 + packages/nextjs/tsconfig.test-d.json | 17 + packages/nextjs/vite.config.ts | 4 + 8 files changed, 269 insertions(+), 360 deletions(-) create mode 100644 packages/nextjs/test/config/buildOptions.test-d.ts create mode 100644 packages/nextjs/tsconfig.test-d.json diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 1786b90849eb..b6520e2bf5fd 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -1,9 +1,9 @@ import type { + BuildTimeOptionsBase, GLOBAL_OBJ, ModuleMetadata, ModuleMetadataCallback, ReactComponentAnnotationOptions, - ResolveSourceMapHook, } from '@sentry/core'; // The first argument to `withSentryConfig` (which is the user's next config). @@ -163,305 +163,42 @@ export type SentryBuildWebpackOptions = { reactComponentAnnotation?: ReactComponentAnnotationOptions; // TODO(v12): remove this option }; -export type SentryBuildOptions = { - /** - * The slug of the Sentry organization associated with the app. - * - * This value can also be specified via the `SENTRY_ORG` environment variable. - */ - org?: string; - +// TODO: `silent` and `debug` are currently only respected by the bundler plugin, not by the SDK's own +// build-time code. +/** + * Build-time options for the Sentry Next.js SDK, passed as the second argument to `withSentryConfig`. + * + * This builds on {@link BuildTimeOptionsBase}, the option set shared across Sentry's meta-framework + * SDKs. Options are only overridden below where Next.js genuinely deviates — either in shape + * (`project`, `sourcemaps`, `release`) or in behavior that differs between webpack and Turbopack. + */ +export type SentryBuildOptions = Omit< + BuildTimeOptionsBase, + 'project' | 'sourcemaps' | 'release' | 'buildTimeInstrumentation' | 'applicationKey' | 'moduleMetadata' +> & { /** * The slug of the Sentry project associated with the app. * + * Multiple projects can be passed to upload the build's source maps to each of them. + * * This value can also be specified via the `SENTRY_PROJECT` environment variable. */ project?: string | string[]; - /** - * The authentication token to use for all communication with Sentry. - * Can be obtained from https://sentry.io/orgredirect/organizations/:orgslug/settings/auth-tokens/. - * - * This value can also be specified via the `SENTRY_AUTH_TOKEN` environment variable. - */ - authToken?: string; - - /** - * The base URL of your Sentry instance. Use this if you are using a self-hosted - * or Sentry instance other than sentry.io. - * - * This value can also be set via the `SENTRY_URL` environment variable. - * - * Defaults to https://sentry.io/, which is the correct value for SaaS customers. - */ - sentryUrl?: string; - - /** - * Headers added to every outgoing network request. - */ - headers?: Record; - - /** - * If set to true, internal plugin errors and performance data will be sent to Sentry. - * - * At Sentry we like to use Sentry ourselves to deliver faster and more stable products. - * We're very careful of what we're sending. We won't collect anything other than error - * and high-level performance data. We will never collect your code or any details of the - * projects in which you're using this plugin. - * - * Defaults to `true`. - */ - telemetry?: boolean; - - /** - * Suppresses all Sentry SDK build logs. - * - * Defaults to `false`. - */ - // TODO: Actually implement this for the non-plugin code. - silent?: boolean; - - /** - * Prints additional debug information about the SDK and uploading source maps when building the application. - * - * Defaults to `false`. - */ - // TODO: Actually implement this for the non-plugin code. - debug?: boolean; - /** * Options for source maps uploading. */ - sourcemaps?: { - /** - * Disable any functionality related to source maps. - */ - disable?: boolean; - - /** - * A glob or an array of globs that specifies the build artifacts that should be uploaded to Sentry. - * - * If this option is not specified, the plugin will try to upload all JavaScript files and source map files that are created during build. - * - * The globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob) - * - * Use the `debug` option to print information about which files end up being uploaded. - */ - assets?: string | string[]; - - /** - * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. - * - * The SDK automatically ignores Next.js internal files that don't have source maps (such as manifest files) - * to prevent "Could not determine source map" warnings. Your custom patterns are merged with these defaults. - * - * The globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob) - * - * Use the `debug` option to print information about which files end up being uploaded. - */ - ignore?: string | string[]; - - /** - * Toggle whether generated source maps within your Next.js build folder should be automatically deleted after being uploaded to Sentry. - * - * Defaults to `true`. - */ - deleteSourcemapsAfterUpload?: boolean; - - /** - * A glob or an array of globs that specifies which source map files should be deleted after being uploaded to Sentry. - * - * When set, this overrides the default deletion behavior of `deleteSourcemapsAfterUpload`. - * - * Use this option when you need fine-grained control over which source maps are deleted. - * - * @example - * ```javascript - * withSentryConfig(nextConfig, { - * sourcemaps: { - * filesToDeleteAfterUpload: ['.next/static/**\/*.map'], - * }, - * }); - * ``` - */ - filesToDeleteAfterUpload?: string | string[]; - - /** - * Hook to rewrite the `sources` field inside the source map before being uploaded to Sentry. Does not modify the actual source map. - * - * The hook receives the following arguments: - * - `source` - the source file path from the source map's `sources` field - * - `map` - the source map object - * - `context` - an optional object containing `mapDir`, the absolute path to the directory of the source map file - * - * If not provided, the SDK defaults to stripping webpack-specific prefixes (`webpack://_N_E/`). - * - * Defaults to making all sources relative to `process.cwd()` while building. - */ - // oxlint-disable-next-line typescript-eslint/no-explicit-any -- matches the bundler plugin's RewriteSourcesHook type - rewriteSources?: (source: string, map: any, context?: { mapDir: string }) => string; - - /** - * Hook to customize source map file resolution. - * - * Mostly helpful for complex builds with custom source map generation. For example, if source maps - * are written to a separate directory and the `//# sourceMappingURL=` comment is rewritten to - * something other than a relative path, Sentry is unable to locate the source map for a given - * build artifact. This hook lets you implement the resolution process yourself. - */ - resolveSourceMap?: ResolveSourceMapHook; - }; + sourcemaps?: SentryBuildSourceMapsOptions; /** * Options related to managing the Sentry releases for a build. * + * Note that `release.inject` is not configurable. The Next.js SDK always injects the release value + * itself, because the bundler plugin's own release injection breaks the `app` directory. + * * More info: https://docs.sentry.io/product/releases/ */ - release?: { - /** - * Unique identifier for the release you want to create. - * - * This value can also be specified via the `SENTRY_RELEASE` environment variable. - * - * Defaults to automatically detecting a value for your environment. - * This includes values for Cordova, Heroku, AWS CodeBuild, CircleCI, Xcode, and Gradle, and otherwise uses the git `HEAD`'s commit SHA. - * (the latter requires access to git CLI and for the root directory to be a valid repository) - * - * If you didn't provide a value and the plugin can't automatically detect one, no release will be created. - */ - name?: string; - - /** - * Whether the plugin should create a release on Sentry during the build. - * Note that a release may still appear in Sentry even if this is value is `false` because any Sentry event that has a release value attached will automatically create a release. - * (for example via the `inject` option) - * - * Defaults to `true`. - */ - create?: boolean; - - /** - * Whether the Sentry release should be automatically finalized (meaning an end timestamp is added) after the build ends. - * - * Defaults to `true`. - */ - finalize?: boolean; - - /** - * Unique identifier for the distribution, used to further segment your release. - * Usually your build number. - */ - dist?: string; - - /** - * Version control system remote name. - * - * This value can also be specified via the `SENTRY_VSC_REMOTE` environment variable. - * - * Defaults to 'origin'. - */ - vcsRemote?: string; - - /** - * Associates the release with its commits in Sentry. - */ - setCommits?: ( - | { - /** - * Automatically sets `commit` and `previousCommit`. Sets `commit` to `HEAD` - * and `previousCommit` as described in the option's documentation. - * - * If you set this to `true`, manually specified `commit` and `previousCommit` - * options will be overridden. It is best to not specify them at all if you - * set this option to `true`. - */ - auto: true; - - repo?: undefined; - commit?: undefined; - } - | { - auto?: false | undefined; - - /** - * The full repo name as defined in Sentry. - * - * Required if the `auto` option is not set to `true`. - */ - repo: string; - - /** - * The current (last) commit in the release. - * - * Required if the `auto` option is not set to `true`. - */ - commit: string; - } - ) & { - /** - * The commit before the beginning of this release (in other words, - * the last commit of the previous release). - * - * Defaults to the last commit of the previous release in Sentry. - * - * If there was no previous release, the last 10 commits will be used. - */ - previousCommit?: string; - - /** - * If the flag is to `true` and the previous release commit was not found - * in the repository, the plugin creates a release with the default commits - * count instead of failing the command. - * - * Defaults to `false`. - */ - ignoreMissing?: boolean; - - /** - * If this flag is set, the setCommits step will not fail and just exit - * silently if no new commits for a given release have been found. - * - * Defaults to `false`. - */ - ignoreEmpty?: boolean; - }; - - /** - * Adds deployment information to the release in Sentry. - */ - deploy?: { - /** - * Environment for this release. Values that make sense here would - * be `production` or `staging`. - */ - env: string; - - /** - * Deployment start time in Unix timestamp (in seconds) or ISO 8601 format. - */ - started?: number | string; - - /** - * Deployment finish time in Unix timestamp (in seconds) or ISO 8601 format. - */ - finished?: number | string; - - /** - * Deployment duration (in seconds). Can be used instead of started and finished. - */ - time?: number; - - /** - * Human readable name for the deployment. - */ - name?: string; - - /** - * URL that points to the deployment. - */ - url?: string; - }; - }; + release?: Omit, 'inject'>; /** * Automatic instrumentation of server-side dependencies at build time. @@ -486,18 +223,6 @@ export type SentryBuildOptions = { */ applicationKey?: string; - /** - * Options related to react component name annotations. - * Disabled by default, unless a value is set for this option. - * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names. - * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in - * breadcrumbs and performance monitoring. - * - * For webpack builds, this is forwarded to `@sentry/bundler-plugins/webpack`. - * For Turbopack builds, this applies the annotations via a custom loader and requires Next.js 16+. - */ - reactComponentAnnotation?: ReactComponentAnnotationOptions; - /** * Metadata that should be associated with the built application. * @@ -512,48 +237,16 @@ export type SentryBuildOptions = { moduleMetadata?: ModuleMetadata | ModuleMetadataCallback; /** - * Options to configure various bundle size optimizations related to the Sentry SDK. + * Options related to react component name annotations. + * Disabled by default, unless a value is set for this option. + * When enabled, your app's DOM will automatically be annotated during build-time with their respective component names. + * This will unlock the capability to search for Replays in Sentry by component name, as well as see component names in + * breadcrumbs and performance monitoring. + * + * For webpack builds, this is forwarded to `@sentry/bundler-plugins/webpack`. + * For Turbopack builds, this applies the annotations via a custom loader and requires Next.js 16+. */ - bundleSizeOptimizations?: { - /** - * If set to `true`, the Sentry SDK will attempt to treeshake (remove) any debugging code within itself during the build. - * Note that the success of this depends on tree shaking being enabled in your build tooling. - * - * Setting this option to `true` will disable features like the SDK's `debug` option. - */ - excludeDebugStatements?: boolean; - - /** - * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code within itself that is related to tracing and performance monitoring. - * Note that the success of this depends on tree shaking being enabled in your build tooling. - * **Notice:** Do not enable this when you're using any performance monitoring-related SDK features (e.g. `Sentry.startTransaction()`). - */ - excludeTracing?: boolean; - - /** - * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay Shadow DOM recording functionality. - * Note that the success of this depends on tree shaking being enabled in your build tooling. - * - * This option is safe to be used when you do not want to capture any Shadow DOM activity via Sentry Session Replay. - */ - excludeReplayShadowDom?: boolean; - - /** - * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay `iframe` recording functionality. - * Note that the success of this depends on tree shaking being enabled in your build tooling. - * - * You can safely do this when you do not want to capture any `iframe` activity via Sentry Session Replay. - */ - excludeReplayIframe?: boolean; - - /** - * If set to `true`, the Sentry SDK will attempt to treeshake (remove) code related to the SDK's Session Replay's Compression Web Worker. - * Note that the success of this depends on tree shaking being enabled in your build tooling. - * - * **Notice:** You should only use this option if you manually host a compression worker and configure it in your Sentry Session Replay integration config via the `workerUrl` option. - */ - excludeReplayWorker?: boolean; - }; + reactComponentAnnotation?: ReactComponentAnnotationOptions; /** * Include Next.js-internal code and code from dependencies when uploading source maps. @@ -581,23 +274,6 @@ export type SentryBuildOptions = { */ tunnelRoute?: string | boolean; - /** - * When an error occurs during release creation or sourcemaps upload, the plugin will call this function. - * - * By default, the plugin will simply throw an error, thereby stopping the bundling process. - * If an `errorHandler` callback is provided, compilation will continue, unless an error is - * thrown in the provided callback. - * - * To allow compilation to continue but still emit a warning, set this option to the following: - * - * ```js - * (err) => { - * console.warn(err); - * } - * ``` - */ - errorHandler?: (err: Error) => void; - /** * Suppress the warning about the `onRouterTransitionStart` hook. */ @@ -705,6 +381,31 @@ export type SentryBuildOptions = { webpack?: SentryBuildWebpackOptions; }; +type SentryBuildSourceMapsOptions = Omit, 'ignore'> & { + /** + * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. + * + * The SDK automatically ignores Next.js internal files that don't have source maps (such as manifest files) + * to prevent "Could not determine source map" warnings. Your custom patterns are merged with these defaults. + * + * The globbing patterns follow the implementation of the `glob` package. (https://www.npmjs.com/package/glob) + * + * Use the `debug` option to print information about which files end up being uploaded. + */ + ignore?: string | string[]; + + /** + * Toggle whether generated source maps within your Next.js build folder should be automatically deleted after being + * uploaded to Sentry. + * + * Has no effect when `disable` is set to `"disable-upload"`, since nothing is uploaded in that case and the source + * maps are kept around for a manual upload. + * + * Defaults to `true`. + */ + deleteSourcemapsAfterUpload?: boolean; +}; + export type NextConfigFunction = ( phase: string, defaults: { defaultConfig: NextConfigObject }, diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 02d5e7249848..483939fc157e 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -359,7 +359,7 @@ export function constructWebpackConfigFunction({ loadModule<{ sentryWebpackPlugin: any }>('@sentry/bundler-plugins/webpack', module) ?? {}; if (sentryWebpackPlugin) { - if (!userSentryOptions.sourcemaps?.disable) { + if (userSentryOptions.sourcemaps?.disable !== true) { // Source maps can be configured in 3 ways: // 1. (next config): productionBrowserSourceMaps // 2. (next config): experimental.serverSourceMaps @@ -381,8 +381,13 @@ export function constructWebpackConfigFunction({ } } - // enable source map deletion if not explicitly disabled - if (!isServer && userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { + // enable source map deletion if not explicitly disabled - with `'disable-upload'` the source maps are + // kept around on purpose, so that they can be uploaded manually later on + if ( + !isServer && + userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined && + userSentryOptions.sourcemaps?.disable !== 'disable-upload' + ) { debug.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.', ); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index 90b00511c92a..c217fa203229 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -235,7 +235,7 @@ export function maybeEnableTurbopackSourcemaps( bundlerInfo: BundlerInfo, ): void { // Enable source maps for turbopack builds - if (!bundlerInfo.isTurbopackSupported || !bundlerInfo.isTurbopack || userSentryOptions.sourcemaps?.disable) { + if (!bundlerInfo.isTurbopackSupported || !bundlerInfo.isTurbopack || userSentryOptions.sourcemaps?.disable === true) { return; } @@ -250,8 +250,12 @@ export function maybeEnableTurbopackSourcemaps( } incomingUserNextConfigObject.productionBrowserSourceMaps = true; - // Enable source map deletion if not explicitly disabled - if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload !== undefined) { + // Enable source map deletion if not explicitly disabled - with `'disable-upload'` the source maps are kept around + // on purpose, so that they can be uploaded manually later on + if ( + userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload !== undefined || + userSentryOptions.sourcemaps?.disable === 'disable-upload' + ) { return; } diff --git a/packages/nextjs/test/config/buildOptions.test-d.ts b/packages/nextjs/test/config/buildOptions.test-d.ts new file mode 100644 index 000000000000..657e7b5e9dd4 --- /dev/null +++ b/packages/nextjs/test/config/buildOptions.test-d.ts @@ -0,0 +1,123 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import type { SentryBuildOptions } from '../../src/config/types'; + +describe('Sentry Next.js build-time options type', () => { + it('includes all options based on type BuildTimeOptionsBase', () => { + const completeOptions: SentryBuildOptions = { + // --- BuildTimeOptionsBase options --- + org: 'test-org', + project: 'test-project', + authToken: 'test-auth-token', + sentryUrl: 'https://sentry.io', + headers: { Authorization: ' Bearer test-auth-token' }, + telemetry: true, + silent: false, + // eslint-disable-next-line no-console + errorHandler: (err: Error) => console.warn(err), + debug: false, + sourcemaps: { + disable: false, + assets: ['./.next/**/*'], + ignore: ['./.next/*.map'], + filesToDeleteAfterUpload: ['./.next/*.map'], + rewriteSources: (source: string) => source, + resolveSourceMap: (artifactPath: string) => `${artifactPath}.map`, + }, + moduleMetadata: { team: 'sdk' }, + release: { + name: 'test-release-1.0.0', + create: true, + finalize: true, + dist: 'test-dist', + vcsRemote: 'origin', + setCommits: { + auto: false, + repo: 'test/repo', + commit: 'abc123', + previousCommit: 'def456', + ignoreMissing: false, + ignoreEmpty: false, + }, + deploy: { + env: 'production', + started: 1234567890, + finished: 1234567900, + time: 10, + name: 'deployment-name', + url: 'https://example.com', + }, + }, + bundleSizeOptimizations: { + excludeDebugStatements: true, + excludeTracing: false, + excludeReplayShadowDom: true, + excludeReplayIframe: true, + excludeReplayWorker: true, + }, + buildTimeInstrumentation: false, + applicationKey: 'test-application-key', + + // --- SentryBuildOptions specific options --- + reactComponentAnnotation: { enabled: true, ignoredComponents: ['Ignored'] }, + widenClientFileUpload: true, + tunnelRoute: '/monitoring', + suppressOnRouterTransitionStartWarning: true, + routeManifestInjection: { exclude: ['/admin', /^\/internal\//] }, + useRunAfterProductionCompileHook: true, + _experimental: { thirdPartyOriginStackFrames: true, vercelCronsMonitoring: true }, + webpack: { autoInstrumentServerFunctions: true, autoInstrumentMiddleware: false }, + }; + + expectTypeOf(completeOptions).toEqualTypeOf(); + }); + + it('supports the Next.js-specific option shapes', () => { + const options: SentryBuildOptions = { + // Next.js uploads source maps to multiple projects + project: ['project-a', 'project-b'], + sourcemaps: { + // Next.js-only: source maps are deleted from the build folder after upload + deleteSourcemapsAfterUpload: false, + // shared with the base type, but Next.js merges these with its own internal ignore patterns + ignore: '**/custom-ignore/**', + }, + tunnelRoute: true, + routeManifestInjection: false, + }; + + expectTypeOf(options).toEqualTypeOf(); + }); + + it('supports disabling source map upload while keeping debug ID injection', () => { + const options: SentryBuildOptions = { sourcemaps: { disable: 'disable-upload' } }; + + expectTypeOf(options).toEqualTypeOf(); + }); + + it('supports opting out of commit association and deploy creation', () => { + const options: SentryBuildOptions = { release: { setCommits: false, deploy: false } }; + + expectTypeOf(options).toEqualTypeOf(); + }); + + it('rejects `release.inject`, which the SDK controls itself', () => { + const options: SentryBuildOptions = { + release: { + // @ts-expect-error - the SDK always injects the release via its own value injection loader + inject: true, + }, + }; + + expectTypeOf(options).toEqualTypeOf(); + }); + + it('allows partial configuration', () => { + const minimalOptions: SentryBuildOptions = {}; + + expectTypeOf(minimalOptions).toEqualTypeOf(); + + const partialOptions: SentryBuildOptions = { org: 'my-org', project: 'my-project', debug: false }; + + expectTypeOf(partialOptions).toEqualTypeOf(); + }); +}); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index b7d68015d863..9dcc84c673c7 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -93,6 +93,40 @@ describe('constructWebpackConfigFunction()', () => { getBuildPluginOptionsSpy.mockRestore(); }); + it('still generates source maps but keeps them when upload is disabled via `disable: "disable-upload"`', async () => { + const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); + vi.spyOn(core, 'loadModule').mockImplementation(() => ({ + sentryWebpackPlugin: () => ({ + _name: 'sentry-webpack-plugin', + }), + })); + + const finalWebpackConfig = await materializeFinalWebpackConfig({ + exportedNextConfig, + incomingWebpackConfig: clientWebpackConfig, + incomingWebpackBuildContext: clientBuildContext, + sentryBuildTimeOptions: { + sourcemaps: { + disable: 'disable-upload', + }, + }, + }); + + expect(finalWebpackConfig.devtool).toEqual('hidden-source-map'); + expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + // The source maps are meant to be uploaded manually later on, so they must not be deleted + sentryBuildOptions: expect.objectContaining({ + sourcemaps: { + disable: 'disable-upload', + }, + }), + }), + ); + + getBuildPluginOptionsSpy.mockRestore(); + }); + it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when enabled', async () => { const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); vi.spyOn(core, 'loadModule').mockImplementation(() => ({ diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 15a3c33f5f5d..435b6506083c 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -4,6 +4,7 @@ import { filterInstrumentedExternals, ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES, } from '../../src/config/diagnosticsChannelInjection'; +import type { SentryBuildOptions } from '../../src/config/types'; import * as util from '../../src/config/util'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from '../../src/config/withSentryConfig'; import { defaultRuntimePhase, defaultsObject, exportedNextConfig, userNextConfig } from './fixtures'; @@ -583,6 +584,26 @@ describe('withSentryConfig', () => { expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); }); + it('still generates source maps when upload is disabled via `disable: "disable-upload"`', () => { + process.env.TURBOPACK = '1'; + vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); + + const cleanConfig = { ...exportedNextConfig }; + delete cleanConfig.productionBrowserSourceMaps; + + const sentryOptions: SentryBuildOptions = { + sourcemaps: { + disable: 'disable-upload' as const, + }, + }; + + const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); + + expect(finalConfig.productionBrowserSourceMaps).toBe(true); + // The source maps are meant to be uploaded manually later on, so they must not be deleted + expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); + }); + it('preserves explicitly configured deleteSourcemapsAfterUpload setting', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); diff --git a/packages/nextjs/tsconfig.test-d.json b/packages/nextjs/tsconfig.test-d.json new file mode 100644 index 000000000000..35095e45adf5 --- /dev/null +++ b/packages/nextjs/tsconfig.test-d.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + + // Scoped to the type tests only. Vitest reports every diagnostic of the typecheck tsconfig, and the + // rest of the test suite is not type-clean yet. + "include": ["test/**/*.test-d.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node"], + + "target": "es2020", + + // other package-specific, test-specific options + "lib": ["DOM", "ESNext"] + } +} diff --git a/packages/nextjs/vite.config.ts b/packages/nextjs/vite.config.ts index ff64487a9265..9c364d0b2374 100644 --- a/packages/nextjs/vite.config.ts +++ b/packages/nextjs/vite.config.ts @@ -6,5 +6,9 @@ export default defineConfig({ test: { ...baseConfig.test, environment: 'node', + typecheck: { + enabled: true, + tsconfig: './tsconfig.test-d.json', + }, }, }); From 4f989714b045c28d45494682bd56b447111f0084 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 25 Aug 2026 11:18:54 +0200 Subject: [PATCH 2/6] ref(nextjs): Drop stale `debug` TODO from build options type `debug` is respected by the SDK's own build-time code in several places; only `silent` is still plugin-only. Co-Authored-By: Claude Opus 5 (1M context) --- packages/nextjs/src/config/types.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index b6520e2bf5fd..b41237963752 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -163,8 +163,7 @@ export type SentryBuildWebpackOptions = { reactComponentAnnotation?: ReactComponentAnnotationOptions; // TODO(v12): remove this option }; -// TODO: `silent` and `debug` are currently only respected by the bundler plugin, not by the SDK's own -// build-time code. +// TODO: `silent` is only forwarded to the bundler plugin - the SDK's own build-time logging ignores it. /** * Build-time options for the Sentry Next.js SDK, passed as the second argument to `withSentryConfig`. * From 65dbec1ebdd2cc84c583cccedd0cec04b05f69e3 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 25 Aug 2026 11:38:36 +0200 Subject: [PATCH 3/6] fix(nextjs): Don't auto-generate source maps for `disable: 'disable-upload'` Treating `'disable-upload'` as "upload off, everything else on" made the SDK enable source map generation and then skip deletion, leaving `.next/static/**/*.map` served publicly. `'disable-upload'` means the SDK stays out of the source map pipeline entirely and only the bundler plugin's debug ID injection runs, matching `sentryTanstackStart.ts`. Generation is the user's call via `productionBrowserSourceMaps` / `devtool`, and the SDK only auto-deletes what it auto-generated. Co-Authored-By: Claude Opus 5 (1M context) --- packages/nextjs/src/config/types.ts | 4 +- packages/nextjs/src/config/webpack.ts | 11 ++---- .../getFinalConfigObjectBundlerUtils.ts | 10 ++--- .../webpack/constructWebpackConfig.test.ts | 39 +++++++------------ .../test/config/withSentryConfig.test.ts | 9 +++-- 5 files changed, 26 insertions(+), 47 deletions(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index b37430365e6d..4511696dc03f 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -416,8 +416,8 @@ type SentryBuildSourceMapsOptions = Omit('@sentry/bundler-plugins/webpack', module) ?? {}; if (sentryWebpackPlugin) { - if (userSentryOptions.sourcemaps?.disable !== true) { + if (!userSentryOptions.sourcemaps?.disable) { // Source maps can be configured in 3 ways: // 1. (next config): productionBrowserSourceMaps // 2. (next config): experimental.serverSourceMaps @@ -381,13 +381,8 @@ export function constructWebpackConfigFunction({ } } - // enable source map deletion if not explicitly disabled - with `'disable-upload'` the source maps are - // kept around on purpose, so that they can be uploaded manually later on - if ( - !isServer && - userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined && - userSentryOptions.sourcemaps?.disable !== 'disable-upload' - ) { + // enable source map deletion if not explicitly disabled + if (!isServer && userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload === undefined) { debug.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.', ); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index c217fa203229..90b00511c92a 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -235,7 +235,7 @@ export function maybeEnableTurbopackSourcemaps( bundlerInfo: BundlerInfo, ): void { // Enable source maps for turbopack builds - if (!bundlerInfo.isTurbopackSupported || !bundlerInfo.isTurbopack || userSentryOptions.sourcemaps?.disable === true) { + if (!bundlerInfo.isTurbopackSupported || !bundlerInfo.isTurbopack || userSentryOptions.sourcemaps?.disable) { return; } @@ -250,12 +250,8 @@ export function maybeEnableTurbopackSourcemaps( } incomingUserNextConfigObject.productionBrowserSourceMaps = true; - // Enable source map deletion if not explicitly disabled - with `'disable-upload'` the source maps are kept around - // on purpose, so that they can be uploaded manually later on - if ( - userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload !== undefined || - userSentryOptions.sourcemaps?.disable === 'disable-upload' - ) { + // Enable source map deletion if not explicitly disabled + if (userSentryOptions.sourcemaps?.deleteSourcemapsAfterUpload !== undefined) { return; } diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 9dcc84c673c7..96eb751bc697 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -93,38 +93,25 @@ describe('constructWebpackConfigFunction()', () => { getBuildPluginOptionsSpy.mockRestore(); }); - it('still generates source maps but keeps them when upload is disabled via `disable: "disable-upload"`', async () => { - const getBuildPluginOptionsSpy = vi.spyOn(getBuildPluginOptionsModule, 'getBuildPluginOptions'); - vi.spyOn(core, 'loadModule').mockImplementation(() => ({ - sentryWebpackPlugin: () => ({ - _name: 'sentry-webpack-plugin', - }), - })); - - const finalWebpackConfig = await materializeFinalWebpackConfig({ - exportedNextConfig, - incomingWebpackConfig: clientWebpackConfig, - incomingWebpackBuildContext: clientBuildContext, - sentryBuildTimeOptions: { + it('does not auto-enable source map generation when `disable` is "disable-upload"', () => { + const finalNextConfig = materializeFinalNextConfig( + { + ...exportedNextConfig, + webpack: () => ({ ...clientWebpackConfig }) as any, + }, + undefined, + { sourcemaps: { disable: 'disable-upload', }, }, - }); - - expect(finalWebpackConfig.devtool).toEqual('hidden-source-map'); - expect(getBuildPluginOptionsSpy).toHaveBeenCalledWith( - expect.objectContaining({ - // The source maps are meant to be uploaded manually later on, so they must not be deleted - sentryBuildOptions: expect.objectContaining({ - sourcemaps: { - disable: 'disable-upload', - }, - }), - }), ); - getBuildPluginOptionsSpy.mockRestore(); + const finalWebpackConfig = finalNextConfig.webpack?.(clientWebpackConfig, clientBuildContext); + + // The SDK must not generate source maps it will neither upload nor delete - they would be served + // publicly from `.next/static`. Generating them is the user's call via `devtool`. + expect(finalWebpackConfig?.devtool).toBeUndefined(); }); it('passes useRunAfterProductionCompileHook to getBuildPluginOptions when enabled', async () => { diff --git a/packages/nextjs/test/config/withSentryConfig.test.ts b/packages/nextjs/test/config/withSentryConfig.test.ts index 435b6506083c..303b3413f7a8 100644 --- a/packages/nextjs/test/config/withSentryConfig.test.ts +++ b/packages/nextjs/test/config/withSentryConfig.test.ts @@ -584,7 +584,7 @@ describe('withSentryConfig', () => { expect(sentryOptions.sourcemaps).toHaveProperty('deleteSourcemapsAfterUpload', true); }); - it('still generates source maps when upload is disabled via `disable: "disable-upload"`', () => { + it('does not auto-enable source map generation when `disable` is "disable-upload"', () => { process.env.TURBOPACK = '1'; vi.spyOn(util, 'getNextjsVersion').mockReturnValue('15.4.1'); @@ -593,14 +593,15 @@ describe('withSentryConfig', () => { const sentryOptions: SentryBuildOptions = { sourcemaps: { - disable: 'disable-upload' as const, + disable: 'disable-upload', }, }; const finalConfig = materializeFinalNextConfig(cleanConfig, undefined, sentryOptions); - expect(finalConfig.productionBrowserSourceMaps).toBe(true); - // The source maps are meant to be uploaded manually later on, so they must not be deleted + // The SDK must not generate source maps it will neither upload nor delete - they would be served + // publicly from `.next/static`. Generating them is the user's call via `productionBrowserSourceMaps`. + expect(finalConfig.productionBrowserSourceMaps).toBeUndefined(); expect(sentryOptions.sourcemaps).not.toHaveProperty('deleteSourcemapsAfterUpload'); }); From f6538ca84960254a3aa2f100e62a133804dfde82 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 25 Aug 2026 13:48:44 +0200 Subject: [PATCH 4/6] ref(nextjs): Keep Next-specific `filesToDeleteAfterUpload` docs The base type's doc doesn't mention that this option overrides `deleteSourcemapsAfterUpload`, which is Next.js-only. Override it like `ignore` so the precedence stays documented. Co-Authored-By: Claude Opus 5 (1M context) --- packages/nextjs/src/config/types.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 4511696dc03f..6d0a6d16127c 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -399,7 +399,10 @@ export type SentryBuildOptions = Omit< webpack?: SentryBuildWebpackOptions; }; -type SentryBuildSourceMapsOptions = Omit, 'ignore'> & { +type SentryBuildSourceMapsOptions = Omit< + NonNullable, + 'ignore' | 'filesToDeleteAfterUpload' +> & { /** * A glob or an array of globs that specifies which build artifacts should not be uploaded to Sentry. * @@ -412,6 +415,24 @@ type SentryBuildSourceMapsOptions = Omit Date: Tue, 25 Aug 2026 13:50:48 +0200 Subject: [PATCH 5/6] ref(nextjs): Add TODO to collapse the two source map deletion options Co-Authored-By: Claude Opus 5 (1M context) --- packages/nextjs/src/config/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 6d0a6d16127c..4546458cb8ad 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -442,6 +442,8 @@ type SentryBuildSourceMapsOptions = Omit< * * Defaults to `true`. */ + // TODO(v12): Collapse into `filesToDeleteAfterUpload`, which already overrides this and is part of the shared + // build-time options. Two ways to express the same thing is one too many for a public API. deleteSourcemapsAfterUpload?: boolean; }; From 399bbdd89728b7d32f5551fedfa06025265b55d8 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 25 Aug 2026 14:06:20 +0200 Subject: [PATCH 6/6] ref(nextjs): Drop type test and typecheck wiring from this PR The type test needs vitest typecheck enabled, and this package's `tsconfig.test.json` has never been run - it currently has ~80 errors in unrelated test files. Cleaning those up and adding the type test belongs in its own PR rather than riding along with a type refactor. Co-Authored-By: Claude Opus 5 (1M context) --- .../nextjs/test/config/buildOptions.test-d.ts | 123 ------------------ packages/nextjs/tsconfig.test-d.json | 17 --- packages/nextjs/vite.config.ts | 4 - 3 files changed, 144 deletions(-) delete mode 100644 packages/nextjs/test/config/buildOptions.test-d.ts delete mode 100644 packages/nextjs/tsconfig.test-d.json diff --git a/packages/nextjs/test/config/buildOptions.test-d.ts b/packages/nextjs/test/config/buildOptions.test-d.ts deleted file mode 100644 index 657e7b5e9dd4..000000000000 --- a/packages/nextjs/test/config/buildOptions.test-d.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest'; -import type { SentryBuildOptions } from '../../src/config/types'; - -describe('Sentry Next.js build-time options type', () => { - it('includes all options based on type BuildTimeOptionsBase', () => { - const completeOptions: SentryBuildOptions = { - // --- BuildTimeOptionsBase options --- - org: 'test-org', - project: 'test-project', - authToken: 'test-auth-token', - sentryUrl: 'https://sentry.io', - headers: { Authorization: ' Bearer test-auth-token' }, - telemetry: true, - silent: false, - // eslint-disable-next-line no-console - errorHandler: (err: Error) => console.warn(err), - debug: false, - sourcemaps: { - disable: false, - assets: ['./.next/**/*'], - ignore: ['./.next/*.map'], - filesToDeleteAfterUpload: ['./.next/*.map'], - rewriteSources: (source: string) => source, - resolveSourceMap: (artifactPath: string) => `${artifactPath}.map`, - }, - moduleMetadata: { team: 'sdk' }, - release: { - name: 'test-release-1.0.0', - create: true, - finalize: true, - dist: 'test-dist', - vcsRemote: 'origin', - setCommits: { - auto: false, - repo: 'test/repo', - commit: 'abc123', - previousCommit: 'def456', - ignoreMissing: false, - ignoreEmpty: false, - }, - deploy: { - env: 'production', - started: 1234567890, - finished: 1234567900, - time: 10, - name: 'deployment-name', - url: 'https://example.com', - }, - }, - bundleSizeOptimizations: { - excludeDebugStatements: true, - excludeTracing: false, - excludeReplayShadowDom: true, - excludeReplayIframe: true, - excludeReplayWorker: true, - }, - buildTimeInstrumentation: false, - applicationKey: 'test-application-key', - - // --- SentryBuildOptions specific options --- - reactComponentAnnotation: { enabled: true, ignoredComponents: ['Ignored'] }, - widenClientFileUpload: true, - tunnelRoute: '/monitoring', - suppressOnRouterTransitionStartWarning: true, - routeManifestInjection: { exclude: ['/admin', /^\/internal\//] }, - useRunAfterProductionCompileHook: true, - _experimental: { thirdPartyOriginStackFrames: true, vercelCronsMonitoring: true }, - webpack: { autoInstrumentServerFunctions: true, autoInstrumentMiddleware: false }, - }; - - expectTypeOf(completeOptions).toEqualTypeOf(); - }); - - it('supports the Next.js-specific option shapes', () => { - const options: SentryBuildOptions = { - // Next.js uploads source maps to multiple projects - project: ['project-a', 'project-b'], - sourcemaps: { - // Next.js-only: source maps are deleted from the build folder after upload - deleteSourcemapsAfterUpload: false, - // shared with the base type, but Next.js merges these with its own internal ignore patterns - ignore: '**/custom-ignore/**', - }, - tunnelRoute: true, - routeManifestInjection: false, - }; - - expectTypeOf(options).toEqualTypeOf(); - }); - - it('supports disabling source map upload while keeping debug ID injection', () => { - const options: SentryBuildOptions = { sourcemaps: { disable: 'disable-upload' } }; - - expectTypeOf(options).toEqualTypeOf(); - }); - - it('supports opting out of commit association and deploy creation', () => { - const options: SentryBuildOptions = { release: { setCommits: false, deploy: false } }; - - expectTypeOf(options).toEqualTypeOf(); - }); - - it('rejects `release.inject`, which the SDK controls itself', () => { - const options: SentryBuildOptions = { - release: { - // @ts-expect-error - the SDK always injects the release via its own value injection loader - inject: true, - }, - }; - - expectTypeOf(options).toEqualTypeOf(); - }); - - it('allows partial configuration', () => { - const minimalOptions: SentryBuildOptions = {}; - - expectTypeOf(minimalOptions).toEqualTypeOf(); - - const partialOptions: SentryBuildOptions = { org: 'my-org', project: 'my-project', debug: false }; - - expectTypeOf(partialOptions).toEqualTypeOf(); - }); -}); diff --git a/packages/nextjs/tsconfig.test-d.json b/packages/nextjs/tsconfig.test-d.json deleted file mode 100644 index 35095e45adf5..000000000000 --- a/packages/nextjs/tsconfig.test-d.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "./tsconfig.json", - - // Scoped to the type tests only. Vitest reports every diagnostic of the typecheck tsconfig, and the - // rest of the test suite is not type-clean yet. - "include": ["test/**/*.test-d.ts"], - - "compilerOptions": { - // should include all types from `./tsconfig.json` plus types for all test frameworks used - "types": ["node"], - - "target": "es2020", - - // other package-specific, test-specific options - "lib": ["DOM", "ESNext"] - } -} diff --git a/packages/nextjs/vite.config.ts b/packages/nextjs/vite.config.ts index 9c364d0b2374..ff64487a9265 100644 --- a/packages/nextjs/vite.config.ts +++ b/packages/nextjs/vite.config.ts @@ -6,9 +6,5 @@ export default defineConfig({ test: { ...baseConfig.test, environment: 'node', - typecheck: { - enabled: true, - tsconfig: './tsconfig.test-d.json', - }, }, });