diff --git a/packages/nextjs/src/config/buildLogger.ts b/packages/nextjs/src/config/buildLogger.ts new file mode 100644 index 000000000000..002882d9e228 --- /dev/null +++ b/packages/nextjs/src/config/buildLogger.ts @@ -0,0 +1,18 @@ +type BuildLogger = Pick; + +const noop = (): void => { + // noop +}; + +const SILENT_BUILD_LOGGER: BuildLogger = { debug: noop, error: noop, log: noop, warn: noop }; + +/** + * Returns the logger for the SDK's own build-time output, honoring the `silent` build option. + * + * The bundler plugin gates its own logs on `silent` internally, so this only covers messages the + * Next.js SDK prints itself. + */ +export function getBuildLogger(silent: boolean | undefined): BuildLogger { + // eslint-disable-next-line no-console + return silent ? SILENT_BUILD_LOGGER : console; +} diff --git a/packages/nextjs/src/config/getBuildPluginOptions.ts b/packages/nextjs/src/config/getBuildPluginOptions.ts index 8646833b4278..0dc209ee71f7 100644 --- a/packages/nextjs/src/config/getBuildPluginOptions.ts +++ b/packages/nextjs/src/config/getBuildPluginOptions.ts @@ -1,6 +1,7 @@ import type { Options as SentryBuildPluginOptions } from '@sentry/bundler-plugins/core'; import * as fs from 'fs'; import * as path from 'path'; +import { getBuildLogger } from './buildLogger'; import type { SentryBuildOptions } from './types'; const LOGGER_PREFIXES = { @@ -277,6 +278,8 @@ export function getBuildPluginOptions({ buildTool: BuildTool; useRunAfterProductionCompileHook?: boolean; // Whether the user has opted into using the experimental hook }): SentryBuildPluginOptions { + const logger = getBuildLogger(sentryBuildOptions.silent); + // 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 @@ -298,8 +301,7 @@ export function getBuildPluginOptions({ const userFilesToDeleteAfterUpload = sentryBuildOptions.sourcemaps?.filesToDeleteAfterUpload; if (sentryBuildOptions.debug && userFilesToDeleteAfterUpload !== undefined) { - // eslint-disable-next-line no-console - console.debug( + logger.debug( '[@sentry/nextjs] Skipping auto-deletion of source maps as user has provided filesToDeleteAfterUpload:', userFilesToDeleteAfterUpload, ); diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index cbd6b20e5970..51591f8cfc50 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -2,6 +2,7 @@ import type { createSentryBuildPluginManager as createSentryBuildPluginManagerTy import { loadModule } from '@sentry/core'; import * as fs from 'fs'; import * as path from 'path'; +import { getBuildLogger } from './buildLogger'; import { getBuildPluginOptions } from './getBuildPluginOptions'; import type { SentryBuildOptions } from './types'; @@ -25,9 +26,10 @@ export async function handleRunAfterProductionCompile( }, sentryBuildOptions: SentryBuildOptions, ): Promise { + const logger = getBuildLogger(sentryBuildOptions.silent); + if (sentryBuildOptions.debug) { - // eslint-disable-next-line no-console - console.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); + logger.debug('[@sentry/nextjs] Running runAfterProductionCompile logic.'); } const { createSentryBuildPluginManager } = @@ -37,10 +39,7 @@ export async function handleRunAfterProductionCompile( ) ?? {}; if (!createSentryBuildPluginManager) { - // eslint-disable-next-line no-console - console.warn( - '[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.', - ); + logger.warn('[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.'); return; } @@ -80,7 +79,11 @@ export async function handleRunAfterProductionCompile( !sentryBuildOptions.sourcemaps?.assets && options.sourcemaps?.disable !== true ) { - await warnAboutUncoveredSourcemaps(path.join(distDir, 'static'), options.sourcemaps?.assets); + await warnAboutUncoveredSourcemaps( + path.join(distDir, 'static'), + options.sourcemaps?.assets, + sentryBuildOptions.silent, + ); } await sentryBuildPluginManager.deleteArtifacts(); @@ -93,12 +96,11 @@ export async function handleRunAfterProductionCompile( // When SRI is enabled, we must skip this step because Next.js computes integrity // hashes during the build — modifying files afterward invalidates those hashes. if (deleteSourcemapsAfterUpload && buildTool === 'turbopack' && !sriEnabled) { - await stripSourceMappingURLComments(path.join(distDir, 'static'), sentryBuildOptions.debug); + await stripSourceMappingURLComments(path.join(distDir, 'static'), sentryBuildOptions); } if (deleteSourcemapsAfterUpload && buildTool === 'turbopack' && sriEnabled && sentryBuildOptions.debug) { - // eslint-disable-next-line no-console - console.debug( + logger.debug( '[@sentry/nextjs] Skipping sourceMappingURL comment stripping because Subresource Integrity (SRI) is enabled.', ); } @@ -107,6 +109,7 @@ export async function handleRunAfterProductionCompile( async function warnAboutUncoveredSourcemaps( staticDir: string, uploadAssets: string | string[] | undefined, + silent: boolean | undefined, ): Promise { let entries: string[]; try { @@ -126,8 +129,7 @@ async function warnAboutUncoveredSourcemaps( .filter(mapPath => !assetPaths.some(assetPath => mapPath === assetPath || mapPath.startsWith(`${assetPath}/`))); if (uncovered.length > 0) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(silent).warn( `[@sentry/nextjs] Found ${uncovered.length} source map file(s) under "${staticDir}" (e.g. "${ uncovered[0] }") that are not covered by the source map upload patterns and will be deleted without having been uploaded to Sentry. Stack traces for the affected files will not be symbolicated. Set the \`sourcemaps.assets\` option in \`withSentryConfig\` to cover these files.`, @@ -142,7 +144,10 @@ const CSS_SOURCEMAPPING_URL_COMMENT_REGEX = /\n?\/\*[#@] sourceMappingURL=[^\n]+ * Strips sourceMappingURL comments from all JS/MJS/CJS/CSS files in the given directory. * This prevents browsers from requesting deleted .map files. */ -export async function stripSourceMappingURLComments(staticDir: string, debug?: boolean): Promise { +export async function stripSourceMappingURLComments( + staticDir: string, + { debug, silent }: Pick = {}, +): Promise { let entries: string[]; try { entries = await fs.promises.readdir(staticDir, { recursive: true }).then(e => e.map(f => String(f))); @@ -179,8 +184,7 @@ export async function stripSourceMappingURLComments(staticDir: string, debug?: b const strippedCount = results.filter(Boolean).length; if (debug && strippedCount > 0) { - // eslint-disable-next-line no-console - console.debug( + getBuildLogger(silent).debug( `[@sentry/nextjs] Stripped sourceMappingURL comments from ${String(strippedCount)} file(s) to prevent requests for deleted source maps.`, ); } diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index d2de95c3da36..8d6e619ac751 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -35,6 +35,11 @@ const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrappe const routeHandlerWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'routeHandlerWrapperTemplate.js'); const routeHandlerWrapperTemplateCode = fs.readFileSync(routeHandlerWrapperTemplatePath, { encoding: 'utf8' }); +// NOTE: This file must not import anything from outside `src/config/loaders`. The loaders are their +// own rollup entry point built with `preserveModules`, so emitted paths are relative to the module +// graph's common root - an outside import moves that root up and nests every loader one directory +// deeper, breaking the `path.resolve(__dirname, 'loaders', ...)` lookups in `webpack.ts`. That is why +// `silent` is checked inline here rather than via the shared `getBuildLogger` helper. export type WrappingLoaderOptions = { pagesDir: string | undefined; appDir: string | undefined; @@ -44,6 +49,7 @@ export type WrappingLoaderOptions = { vercelCronsConfig?: VercelCronsConfig; nextjsRequestAsyncStorageModulePath?: string; isDev?: boolean; + silent?: boolean; }; /** @@ -68,6 +74,7 @@ export default function wrappingLoader( vercelCronsConfig, nextjsRequestAsyncStorageModulePath, isDev, + silent, } = 'getOptions' in this ? this.getOptions() : this.query; this.async(); @@ -163,7 +170,7 @@ export default function wrappingLoader( nextjsRequestAsyncStorageModulePath, ); } else { - if (!showedMissingAsyncStorageModuleWarning) { + if (!showedMissingAsyncStorageModuleWarning && !silent) { // eslint-disable-next-line no-console console.warn( "[@sentry/nextjs] The Sentry SDK could not access the 'RequestAsyncStorage' module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.", @@ -227,10 +234,12 @@ export default function wrappingLoader( this.callback(null, wrappedCode, wrappedCodeSourceMap); }) .catch(err => { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:\n${err}`, - ); + if (!silent) { + // eslint-disable-next-line no-console + console.warn( + `[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:\n${err}`, + ); + } this.callback(null, userCode, userModuleSourceMap); }); } diff --git a/packages/nextjs/src/config/manifest/createRouteManifest.ts b/packages/nextjs/src/config/manifest/createRouteManifest.ts index d7f6ad59428e..bf1765bfce4f 100644 --- a/packages/nextjs/src/config/manifest/createRouteManifest.ts +++ b/packages/nextjs/src/config/manifest/createRouteManifest.ts @@ -1,5 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; +import { getBuildLogger } from '../buildLogger'; import type { RouteInfo, RouteManifest } from './types'; /** @@ -25,6 +26,10 @@ export type CreateRouteManifestOptions = { * Pass an empty array to disable optional prefix matching entirely. */ localeParamNames?: string[]; + /** + * Suppresses the SDK's own build-time logs. + */ + silent?: boolean; }; let manifestCache: RouteManifest | null = null; @@ -149,6 +154,7 @@ function scanAppDirectory( basePath: string = '', includeRouteGroups: boolean = false, localeParamNames: string[] = DEFAULT_LOCALE_PARAM_NAMES, + silent?: boolean, ): RouteManifest { const dynamicRoutes: RouteInfo[] = []; const staticRoutes: RouteInfo[] = []; @@ -207,7 +213,7 @@ function scanAppDirectory( } const newBasePath = routeSegment ? `${basePath}/${routeSegment}` : basePath; - const subRoutes = scanAppDirectory(fullPath, newBasePath, includeRouteGroups, localeParamNames); + const subRoutes = scanAppDirectory(fullPath, newBasePath, includeRouteGroups, localeParamNames, silent); dynamicRoutes.push(...subRoutes.dynamicRoutes); staticRoutes.push(...subRoutes.staticRoutes); @@ -215,8 +221,7 @@ function scanAppDirectory( } } } catch (error) { - // eslint-disable-next-line no-console - console.warn('Error building route manifest:', error); + getBuildLogger(silent).warn('[@sentry/nextjs] Error building route manifest:', error); } return { dynamicRoutes, staticRoutes, isrRoutes }; @@ -267,6 +272,7 @@ export function createRouteManifest(options?: CreateRouteManifestOptions): Route options?.basePath, options?.includeRouteGroups, localeParamNames, + options?.silent, ); const manifest: RouteManifest = { diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 8c2c392f7a6a..d6efd7f5a5f0 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -7,6 +7,7 @@ import { serializeInstrumentations, } from '@sentry/server-utils/orchestrion/webpack'; import type { VercelCronsConfig } from '../../common/types'; +import { getBuildLogger } from '../buildLogger'; import type { RouteManifest } from '../manifest/types'; import type { JSONValue, @@ -124,8 +125,7 @@ export function constructTurbopackConfig({ } else { // Without this warning the option silently no-ops, which is indistinguishable from // annotation being broken. - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions?.silent).warn( `[@sentry/nextjs] \`reactComponentAnnotation\` is enabled but React component annotation requires Next.js 16+ on Turbopack builds${ nextJsVersion ? ` (detected ${nextJsVersion})` : '' }. Your components will not be annotated.`, diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 4546458cb8ad..6750f2d43fff 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -163,7 +163,6 @@ export type SentryBuildWebpackOptions = { reactComponentAnnotation?: ReactComponentAnnotationOptions; // TODO(v12): remove this option }; -// 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`. * diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 02d5e7249848..4076c062fbc1 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -6,6 +6,7 @@ import * as fs from 'fs'; import { createRequire } from 'module'; import * as path from 'path'; import type { VercelCronsConfig } from '../common/types'; +import { getBuildLogger } from './buildLogger'; import { externalizeOrchestrionRuntimePackages } from './diagnosticsChannelInjection'; import { getBuildPluginOptions, normalizePathForGlob } from './getBuildPluginOptions'; import type { RouteManifest } from './manifest/types'; @@ -68,6 +69,7 @@ export function constructWebpackConfigFunction({ buildContext: BuildContext, ): WebpackConfigObject { const { isServer, dev: isDev, dir: projectDir } = buildContext; + const logger = getBuildLogger(userSentryOptions.silent); const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'server') : 'client'; // Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161 const pageExtensions = userNextConfig.pageExtensions || ['tsx', 'ts', 'jsx', 'js']; @@ -82,12 +84,12 @@ export function constructWebpackConfigFunction({ const instrumentationFile = getInstrumentationFile(projectDir, dotPrefixedPageExtensions.concat(['.ts', '.js'])); if (runtime !== 'client') { - warnAboutDeprecatedConfigFiles(projectDir, instrumentationFile, runtime); + warnAboutDeprecatedConfigFiles(projectDir, instrumentationFile, runtime, userSentryOptions.silent); } if (runtime === 'server') { // was added in v15 (https://github.com/vercel/next.js/pull/67539) if (major && major >= 15) { - warnAboutMissingOnRequestErrorHandler(instrumentationFile); + warnAboutMissingOnRequestErrorHandler(instrumentationFile, userSentryOptions.silent); } } @@ -160,6 +162,7 @@ export function constructWebpackConfigFunction({ rawNewConfig.resolve?.modules, ), isDev, + silent: userSentryOptions.silent, }; const normalizeLoaderResourcePath = (resourcePath: string): string => { @@ -320,8 +323,7 @@ export function constructWebpackConfigFunction({ !showedMissingGlobalErrorWarningMsg && !process.env.SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING ) { - // eslint-disable-next-line no-console - console.log( + logger.log( "[@sentry/nextjs] It seems like you don't have a global error handler set up. It is recommended that you add a 'global-error.js' file with Sentry instrumentation so that React rendering errors are reported to Sentry. Read more: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#react-render-errors-in-app-router (you can suppress this warning by setting SENTRY_SUPPRESS_GLOBAL_ERROR_HANDLER_FILE_WARNING=1 as environment variable)", ); showedMissingGlobalErrorWarningMsg = true; @@ -338,12 +340,12 @@ export function constructWebpackConfigFunction({ // will call the callback which will call `f` which will call `x.y`... and on and on. Theoretically this could also // be fixed by using `bind`, but this is way simpler.) const origEntryProperty = newConfig.entry; - newConfig.entry = async () => addSentryToClientEntryProperty(origEntryProperty, buildContext); + newConfig.entry = async () => + addSentryToClientEntryProperty(origEntryProperty, buildContext, userSentryOptions.silent); const clientSentryConfigFileName = getClientSentryConfigFile(projectDir); if (clientSentryConfigFileName) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( `[@sentry/nextjs] DEPRECATION WARNING: It is recommended renaming your \`${clientSentryConfigFileName}\` file, or moving its content to \`instrumentation-client.ts\`. When using Turbopack \`${clientSentryConfigFileName}\` will no longer work. Read more about the \`instrumentation-client.ts\` file: https://nextjs.org/docs/app/api-reference/file-conventions/instrumentation-client`, ); } @@ -450,6 +452,7 @@ export function constructWebpackConfigFunction({ async function addSentryToClientEntryProperty( currentEntryProperty: WebpackEntryProperty, buildContext: BuildContext, + silent?: boolean, ): Promise { // The `entry` entry in a webpack config can be a string, array of strings, object, or function. By default, nextjs // sets it to an async function which returns the promise of an object of string arrays. Because we don't know whether @@ -482,7 +485,7 @@ async function addSentryToClientEntryProperty( // entrypoint for `/app` pages entryPointName === 'main-app' ) { - addFilesToWebpackEntryPoint(newEntryProperty, entryPointName, filesToInject, isDevMode); + addFilesToWebpackEntryPoint(newEntryProperty, entryPointName, filesToInject, isDevMode, silent); } } @@ -512,11 +515,12 @@ function getInstrumentationFile(projectDir: string, dotPrefixedExtensions: strin /** * Make sure the instrumentation file has a `onRequestError` Handler */ -function warnAboutMissingOnRequestErrorHandler(instrumentationFile: string | null): void { +function warnAboutMissingOnRequestErrorHandler(instrumentationFile: string | null, silent?: boolean): void { + const logger = getBuildLogger(silent); + if (!instrumentationFile) { if (!process.env.SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( '[@sentry/nextjs] Could not find a Next.js instrumentation file. This indicates an incomplete configuration of the Sentry SDK. An instrumentation file is required for the Sentry SDK to be initialized on the server: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#create-initialization-config-files (you can suppress this warning by setting SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING=1 as environment variable)', ); } @@ -524,8 +528,7 @@ function warnAboutMissingOnRequestErrorHandler(instrumentationFile: string | nul } if (!instrumentationFile.includes('onRequestError')) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( '[@sentry/nextjs] Could not find `onRequestError` hook in instrumentation file. This indicates outdated configuration of the Sentry SDK. Use `Sentry.captureRequestError` to instrument the `onRequestError` hook: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#errors-from-nested-react-server-components', ); } @@ -542,6 +545,7 @@ function warnAboutDeprecatedConfigFiles( projectDir: string, instrumentationFile: string | null, platform: 'server' | 'edge', + silent?: boolean, ): void { const hasInstrumentationHookWithIndicationsOfSentry = instrumentationFile && @@ -554,8 +558,7 @@ function warnAboutDeprecatedConfigFiles( for (const filename of [`sentry.${platform}.config.ts`, `sentry.${platform}.config.js`]) { if (fs.existsSync(path.resolve(projectDir, filename))) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(silent).warn( `[@sentry/nextjs] It appears you've configured a \`${filename}\` file. Please ensure to put this file's content into the \`register()\` function of a Next.js instrumentation file instead. To ensure correct functionality of the SDK, \`Sentry.init\` must be called inside of an instrumentation file. Learn more about setting up an instrumentation file in Next.js: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation. You can safely delete the \`${filename}\` file afterward.`, ); } @@ -609,6 +612,7 @@ function addFilesToWebpackEntryPoint( entryPointName: string, filesToInsert: string[], isDevMode: boolean, + silent?: boolean, ): void { // BIG FAT NOTE: Order of insertion seems to matter here. If we insert the new files before the `currentEntrypoint`s, // the Next.js dev server breaks. Because we generally still want the SDK to be initialized as early as possible we @@ -653,11 +657,9 @@ function addFilesToWebpackEntryPoint( import: newImportValue, }; } - // malformed entry point (use `console.error` rather than `debug.error` because it will always be printed, regardless - // of SDK settings) + // malformed entry point (printed regardless of `debug`, since it means SDK init was not injected at all) else { - // eslint-disable-next-line no-console - console.error( + getBuildLogger(silent).error( 'Sentry Logger [Error]:', `Could not inject SDK initialization code into entry point ${entryPointName}, as its current value is not in a recognized format.\n`, 'Expected: string | Array | { [key:string]: any, import: string | Array }\n', diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts index d4ffa9fa901b..7e6e33682af8 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -40,7 +40,7 @@ export function getFinalConfigObject( maybeSetUpTunnelRouteRewriteRules(incomingUserNextConfigObject, userSentryOptions); - if (shouldReturnEarlyInExperimentalBuildMode()) { + if (shouldReturnEarlyInExperimentalBuildMode(userSentryOptions.silent)) { return incomingUserNextConfigObject; } @@ -51,12 +51,12 @@ export function getFinalConfigObject( const nextJsVersion = getNextjsVersion(); const nextMajor = getNextMajor(nextJsVersion); - maybeSetClientTraceMetadataOption(incomingUserNextConfigObject, nextJsVersion); - maybeSetInstrumentationHookOption(incomingUserNextConfigObject, nextJsVersion); + maybeSetClientTraceMetadataOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent); + maybeSetInstrumentationHookOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent); warnIfMissingOnRouterTransitionStartHook(userSentryOptions); const bundlerInfo = getBundlerInfo(nextJsVersion); - maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo); + maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo, userSentryOptions.silent); maybeWarnAboutTurbopackModuleMetadata(userSentryOptions, bundlerInfo); maybeWarnAboutUnsupportedRunAfterProductionCompileHook(nextJsVersion, userSentryOptions, bundlerInfo); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts index 90b00511c92a..60b2d3e96538 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectBundlerUtils.ts @@ -3,6 +3,7 @@ import { filterInstrumentedExternals, ORCHESTRION_RUNTIME_EXTERNAL_PACKAGES, } from '../diagnosticsChannelInjection'; +import { getBuildLogger } from '../buildLogger'; import { handleRunAfterProductionCompile } from '../handleRunAfterProductionCompile'; import type { RouteManifest } from '../manifest/types'; import { constructTurbopackConfig } from '../turbopack'; @@ -36,11 +37,14 @@ export function getBundlerInfo(nextJsVersion: string | undefined): BundlerInfo { /** * Warns if turbopack is in use but the detected Next.js version is unsupported. */ -export function maybeWarnAboutUnsupportedTurbopack(nextJsVersion: string | undefined, bundlerInfo: BundlerInfo): void { +export function maybeWarnAboutUnsupportedTurbopack( + nextJsVersion: string | undefined, + bundlerInfo: BundlerInfo, + silent?: boolean, +): void { // Warn if using turbopack with an unsupported Next.js version if (!bundlerInfo.isTurbopackSupported && bundlerInfo.isTurbopack) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(silent).warn( `[@sentry/nextjs] WARNING: You are using the Sentry SDK with Turbopack. The Sentry SDK is compatible with Turbopack on Next.js version 15.4.1 or later. You are currently on ${nextJsVersion}. Please upgrade to a newer Next.js version to use the Sentry SDK with Turbopack.`, ); } @@ -58,8 +62,7 @@ export function maybeWarnAboutTurbopackModuleMetadata( bundlerInfo: BundlerInfo, ): void { if (bundlerInfo.isTurbopack && userSentryOptions.moduleMetadata) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions.silent).warn( '[@sentry/nextjs] WARNING: `moduleMetadata` is currently only applied on webpack builds and has no effect on Turbopack builds. Use `applicationKey` if you need `thirdPartyErrorFilterIntegration` support, which works on both bundlers.', ); } @@ -79,8 +82,7 @@ export function maybeWarnAboutUnsupportedRunAfterProductionCompileHook( !supportsProductionCompileHook(nextJsVersion ?? '') && bundlerInfo.isWebpack ) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions.silent).warn( '[@sentry/nextjs] The configured `useRunAfterProductionCompileHook` option is not compatible with your current Next.js version. This option is only supported on Next.js version 15.4.1 or later. Will not run source map and release management logic.', ); } @@ -218,8 +220,7 @@ export function maybeSetUpRunAfterProductionCompileHook({ return; } - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions.silent).warn( '[@sentry/nextjs] The configured `compiler.runAfterProductionCompile` option is not a function. Will not run source map and release management logic.', ); } @@ -244,9 +245,10 @@ export function maybeEnableTurbopackSourcemaps( return; } + const logger = getBuildLogger(userSentryOptions.silent); + if (userSentryOptions.debug) { - // eslint-disable-next-line no-console - console.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); + logger.log('[@sentry/nextjs] Automatically enabling browser source map generation for turbopack build.'); } incomingUserNextConfigObject.productionBrowserSourceMaps = true; @@ -256,8 +258,7 @@ export function maybeEnableTurbopackSourcemaps( } if (userSentryOptions.debug) { - // eslint-disable-next-line no-console - console.warn( + logger.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/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index f1022a6c7ff2..c457cb75aaf0 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -3,6 +3,7 @@ import { getSentryRelease } from '@sentry/node'; import * as fs from 'fs'; import * as path from 'path'; import type { VercelCronsConfig } from '../../common/types'; +import { getBuildLogger } from '../buildLogger'; import { createRouteManifest } from '../manifest/createRouteManifest'; import type { RouteManifest } from '../manifest/types'; import type { NextConfigObject, SentryBuildOptions } from '../types'; @@ -41,8 +42,7 @@ export function maybeSetUpTunnelRouteRewriteRules( if (incomingUserNextConfigObject.output === 'export') { if (!showedExportModeTunnelWarning) { showedExportModeTunnelWarning = true; - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions.silent).warn( '[@sentry/nextjs] The Sentry Next.js SDK `tunnelRoute` option will not work in combination with Next.js static exports. The `tunnelRoute` option uses server-side features that cannot be accessed in export mode. If you still want to tunnel Sentry events, set up your own tunnel: https://docs.sentry.io/platforms/javascript/troubleshooting/#using-the-tunnel-option', ); } @@ -61,15 +61,14 @@ export function maybeSetUpTunnelRouteRewriteRules( * * @returns `true` if Sentry config processing should be skipped for the current process invocation */ -export function shouldReturnEarlyInExperimentalBuildMode(): boolean { +export function shouldReturnEarlyInExperimentalBuildMode(silent?: boolean): boolean { if (!process.argv.includes('--experimental-build-mode')) { return false; } if (!showedExperimentalBuildModeWarning) { showedExperimentalBuildModeWarning = true; - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(silent).warn( '[@sentry/nextjs] The Sentry Next.js SDK does not currently fully support next build --experimental-build-mode', ); } @@ -99,6 +98,7 @@ export function maybeCreateRouteManifest( const manifest = createRouteManifest({ basePath: incomingUserNextConfigObject.basePath, localeParamNames: userSentryOptions.routeManifestInjection?.localeParamNames, + silent: userSentryOptions.silent, }); // Apply route exclusion filter if configured @@ -138,6 +138,7 @@ export function filterRouteManifest(manifest: RouteManifest, excludeFilter: Excl export function maybeSetClientTraceMetadataOption( incomingUserNextConfigObject: NextConfigObject, nextJsVersion: string | undefined, + silent?: boolean, ): void { // With Cache Components enabled, the page shell — and therefore the document's `sentry-trace`/ // `baggage` meta tags — can be prerendered and rendered in an async context detached from the @@ -162,8 +163,7 @@ export function maybeSetClientTraceMetadataOption( ]; } } else { - // eslint-disable-next-line no-console - console.log( + getBuildLogger(silent).log( "[@sentry/nextjs] The Sentry SDK was not able to determine your Next.js version. If you are using Next.js version 15 or greater, please add `experimental.clientTraceMetadata: ['sentry-trace', 'baggage']` to your Next.js config to enable pageload tracing for App Router.", ); } @@ -175,13 +175,15 @@ export function maybeSetClientTraceMetadataOption( export function maybeSetInstrumentationHookOption( incomingUserNextConfigObject: NextConfigObject, nextJsVersion: string | undefined, + silent?: boolean, ): void { + const logger = getBuildLogger(silent); + // From Next.js version (15.0.0-canary.124) onwards, Next.js does no longer require the `experimental.instrumentationHook` option and will // print a warning when it is set, so we need to conditionally provide it for lower versions. if (nextJsVersion && requiresInstrumentationHook(nextJsVersion)) { if (incomingUserNextConfigObject.experimental?.instrumentationHook === false) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( '[@sentry/nextjs] You turned off the `experimental.instrumentationHook` option. Note that Sentry will not be initialized if you did not set it up inside `instrumentation.(js|ts)`.', ); } @@ -199,14 +201,12 @@ export function maybeSetInstrumentationHookOption( // If we cannot detect a Next.js version for whatever reason, the sensible default is to set the `experimental.instrumentationHook`, even though it may create a warning. if (incomingUserNextConfigObject.experimental && 'instrumentationHook' in incomingUserNextConfigObject.experimental) { if (incomingUserNextConfigObject.experimental.instrumentationHook === false) { - // eslint-disable-next-line no-console - console.warn( + logger.warn( '[@sentry/nextjs] You set `experimental.instrumentationHook` to `false`. If you are using Next.js version 15 or greater, you can remove that option. If you are using Next.js version 14 or lower, you need to set `experimental.instrumentationHook` in your `next.config.(js|mjs)` to `true` for the SDK to be properly initialized in combination with `instrumentation.(js|ts)`.', ); } } else { - // eslint-disable-next-line no-console - console.log( + logger.log( "[@sentry/nextjs] The Sentry SDK was not able to determine your Next.js version. If you are using Next.js version 15 or greater, Next.js will probably show you a warning about the `experimental.instrumentationHook` being set. To silence Next.js' warning, explicitly set the `experimental.instrumentationHook` option in your `next.config.(js|mjs|ts)` to `undefined`. If you are on Next.js version 14 or lower, you can silence this particular warning by explicitly setting the `experimental.instrumentationHook` option in your `next.config.(js|mjs)` to `true`.", ); incomingUserNextConfigObject.experimental = { @@ -227,8 +227,7 @@ export function warnIfMissingOnRouterTransitionStartHook(userSentryOptions: Sent !instrumentationClientFileContents.includes('onRouterTransitionStart') && !userSentryOptions.suppressOnRouterTransitionStartWarning ) { - // eslint-disable-next-line no-console - console.warn( + getBuildLogger(userSentryOptions.silent).warn( '[@sentry/nextjs] ACTION REQUIRED: To instrument navigations, the Sentry SDK requires you to export an `onRouterTransitionStart` hook from your `instrumentation-client.(js|ts)` file. You can do so by adding `export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;` to the file.', ); } diff --git a/packages/nextjs/src/config/withSentryConfig/index.ts b/packages/nextjs/src/config/withSentryConfig/index.ts index a0b2f04950e0..5151e0400b73 100644 --- a/packages/nextjs/src/config/withSentryConfig/index.ts +++ b/packages/nextjs/src/config/withSentryConfig/index.ts @@ -1,4 +1,5 @@ import { isThenable, warnOnRemovedBuildOptions } from '@sentry/core'; +import { getBuildLogger } from '../buildLogger'; import type { ExportedNextConfig as NextConfig, NextConfigFunction, SentryBuildOptions } from '../types'; import { DEFAULT_SERVER_EXTERNAL_PACKAGES } from './constants'; import { getFinalConfigObject } from './getFinalConfigObject'; @@ -15,8 +16,9 @@ export { DEFAULT_SERVER_EXTERNAL_PACKAGES }; * @returns The wrapped Next.js config (same shape as the input) */ export function withSentryConfig(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C { - warnOnRemovedBuildOptions(sentryBuildOptions, ['unstable_sentryWebpackPluginOptions']); - warnOnRemovedBuildOptions(sentryBuildOptions.webpack, ['unstable_sentryWebpackPluginOptions']); + const logWarning = (message: string): void => getBuildLogger(sentryBuildOptions.silent).warn(message); + warnOnRemovedBuildOptions(sentryBuildOptions, ['unstable_sentryWebpackPluginOptions'], logWarning); + warnOnRemovedBuildOptions(sentryBuildOptions.webpack, ['unstable_sentryWebpackPluginOptions'], logWarning); const castNextConfig = (nextConfig as NextConfig) || {}; if (typeof castNextConfig === 'function') { diff --git a/packages/nextjs/test/config/silent.test.ts b/packages/nextjs/test/config/silent.test.ts new file mode 100644 index 000000000000..0892fe80dfe1 --- /dev/null +++ b/packages/nextjs/test/config/silent.test.ts @@ -0,0 +1,116 @@ +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getBuildLogger } from '../../src/config/buildLogger'; +import { createRouteManifest } from '../../src/config/manifest/createRouteManifest'; +import type { NextConfigObject } from '../../src/config/types'; +import { withSentryConfig } from '../../src/config/withSentryConfig'; +import type { BundlerInfo } from '../../src/config/withSentryConfig/getFinalConfigObjectBundlerUtils'; +import { + maybeEnableTurbopackSourcemaps, + maybeWarnAboutTurbopackModuleMetadata, + maybeWarnAboutUnsupportedRunAfterProductionCompileHook, + maybeWarnAboutUnsupportedTurbopack, +} from '../../src/config/withSentryConfig/getFinalConfigObjectBundlerUtils'; +import { maybeSetClientTraceMetadataOption } from '../../src/config/withSentryConfig/getFinalConfigObjectUtils'; + +const TURBOPACK_UNSUPPORTED: BundlerInfo = { isTurbopack: true, isWebpack: false, isTurbopackSupported: false }; +const TURBOPACK_SUPPORTED: BundlerInfo = { isTurbopack: true, isWebpack: false, isTurbopackSupported: true }; +const WEBPACK: BundlerInfo = { isTurbopack: false, isWebpack: true, isTurbopackSupported: false }; + +// `createRouteManifest` memoizes per app dir and `silent` is not part of the cache key, so the two +// variants need distinct paths to both actually scan. Neither is a directory, which is what makes it log. +const NOT_A_DIRECTORY = { silent: __filename, notSilent: path.join(__dirname, 'testUtils.ts') }; + +describe('getBuildLogger', () => { + it('forwards to the console when not silent', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + getBuildLogger(false).warn('hello'); + + expect(warnSpy).toHaveBeenCalledWith('hello'); + + warnSpy.mockRestore(); + }); + + it.each(['log', 'warn', 'error', 'debug'] as const)('swallows `%s` when silent', method => { + const spy = vi.spyOn(console, method).mockImplementation(() => {}); + + getBuildLogger(true)[method]('hello'); + + expect(spy).not.toHaveBeenCalled(); + + spy.mockRestore(); + }); +}); + +// `silent` is documented as suppressing *all* SDK build logs, but for a long time it was only +// forwarded to the bundler plugin. These cover the SDK's own build-time output, one case per file +// that logs, so a newly added un-gated `console` call in any of them shows up here. +describe('`silent` build option', () => { + let spies: Array> = []; + + beforeEach(() => { + spies = (['log', 'warn', 'error', 'debug'] as const).map(method => + vi.spyOn(console, method).mockImplementation(() => {}), + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + function countLogs(): number { + return spies.reduce((total, spy) => total + spy.mock.calls.length, 0); + } + + describe.each([ + [ + 'removed build options', + (silent?: boolean) => + // @ts-expect-error - removed in v11, but JS configs get no type checking + withSentryConfig({}, { silent, unstable_sentryWebpackPluginOptions: {} }), + ], + [ + 'unsupported turbopack version', + (silent?: boolean) => maybeWarnAboutUnsupportedTurbopack('15.0.0', TURBOPACK_UNSUPPORTED, silent), + ], + [ + 'moduleMetadata on turbopack', + (silent?: boolean) => maybeWarnAboutTurbopackModuleMetadata({ silent, moduleMetadata: {} }, TURBOPACK_SUPPORTED), + ], + [ + 'unsupported runAfterProductionCompile hook', + (silent?: boolean) => + maybeWarnAboutUnsupportedRunAfterProductionCompileHook( + '15.0.0', + { silent, useRunAfterProductionCompileHook: true }, + WEBPACK, + ), + ], + [ + 'turbopack source map auto-enabling', + (silent?: boolean) => maybeEnableTurbopackSourcemaps({}, { silent, debug: true }, TURBOPACK_SUPPORTED), + ], + [ + 'undetectable Next.js version', + (silent?: boolean) => maybeSetClientTraceMetadataOption({} as NextConfigObject, undefined, silent), + ], + [ + 'unreadable app directory', + (silent?: boolean) => + createRouteManifest({ appDirPath: silent ? NOT_A_DIRECTORY.silent : NOT_A_DIRECTORY.notSilent, silent }), + ], + ])('%s', (_name, run) => { + it('logs when `silent` is not set', () => { + run(undefined); + + expect(countLogs()).toBeGreaterThan(0); + }); + + it('logs nothing when `silent` is `true`', () => { + run(true); + + expect(countLogs()).toBe(0); + }); + }); +});