diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 32bd6bee28e7..b7c1efa5cb65 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -515,19 +515,8 @@ function getInstrumentationFile(projectDir: string, dotPrefixedExtensions: strin * Make sure the instrumentation file has a `onRequestError` Handler */ function warnAboutMissingOnRequestErrorHandler(instrumentationFile: string | null, silent?: boolean): void { - const logger = getBuildLogger(silent); - - if (!instrumentationFile) { - if (!process.env.SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING) { - 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)', - ); - } - return; - } - - if (!instrumentationFile.includes('onRequestError')) { - logger.warn( + if (instrumentationFile && !instrumentationFile.includes('onRequestError')) { + getBuildLogger(silent).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', ); } diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts index fb1b9f616aeb..e855041202e1 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObject.ts @@ -25,6 +25,7 @@ import { maybeSetUpTunnelRouteRewriteRules, resolveReleaseName, shouldReturnEarlyInExperimentalBuildMode, + warnIfMissingInstrumentationFile, warnIfMissingOnRouterTransitionStartHook, } from './getFinalConfigObjectUtils'; @@ -55,6 +56,7 @@ export function getFinalConfigObject( maybeSetClientTraceMetadataOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent); maybeSetInstrumentationHookOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent); warnIfMissingOnRouterTransitionStartHook(userSentryOptions); + warnIfMissingInstrumentationFile(incomingUserNextConfigObject, userSentryOptions); const bundlerInfo = getBundlerInfo(nextJsVersion); maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo, userSentryOptions.silent); diff --git a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts index 68520bc1e16d..7ca66c51d1f7 100644 --- a/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts +++ b/packages/nextjs/src/config/withSentryConfig/getFinalConfigObjectUtils.ts @@ -7,7 +7,7 @@ import { getBuildLogger } from '../buildLogger'; import { createRouteManifest } from '../manifest/createRouteManifest'; import type { RouteManifest } from '../manifest/types'; import type { NextConfigObject, SentryBuildOptions } from '../types'; -import { requiresInstrumentationHook } from '../util'; +import { DEFAULT_PAGE_EXTENSIONS, requiresInstrumentationHook } from '../util'; import { getGitRevision, getInstrumentationClientFileContents } from './buildTime'; import { findTunnelRouteHandlerFile, @@ -21,6 +21,8 @@ let showedUnusedTunnelRouteHandlerWarning = false; let showedTunnelRouteHandlerModeLog = false; let showedExperimentalBuildModeWarning = false; +const MISSING_INSTRUMENTATION_FILE_WARNING_SHOWN = '__SENTRY_MISSING_INSTRUMENTATION_FILE_WARNING_SHOWN__'; + /** * Resolves the Sentry release name to use for build-time behavior. * @@ -266,6 +268,43 @@ export function warnIfMissingOnRouterTransitionStartHook(userSentryOptions: Sent } } +/** + * Warns if there is no `instrumentation` file next to the `app` or `pages` directory. + * `next build` only looks there, even though dev and Turbopack also find the file in other places. + */ +export function warnIfMissingInstrumentationFile( + incomingUserNextConfigObject: NextConfigObject, + userSentryOptions: SentryBuildOptions, +): void { + if ( + process.env[MISSING_INSTRUMENTATION_FILE_WARNING_SHOWN] || + process.env.SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING + ) { + return; + } + + const projectDir = process.cwd(); + const exists = (relativePath: string): boolean => fs.existsSync(path.join(projectDir, relativePath)); + + // Same order as Next.js' `findPagesDir` + const routerDir = ['pages', path.join('src', 'pages'), 'app', path.join('src', 'app')].find(exists); + if (!routerDir) { + return; + } + + const instrumentationDir = path.dirname(routerDir); + const pageExtensions = incomingUserNextConfigObject.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS; + if (pageExtensions.some(extension => exists(path.join(instrumentationDir, `instrumentation.${extension}`)))) { + return; + } + + // Next.js may evaluate its config in child processes, which inherit this state from their parent. + process.env[MISSING_INSTRUMENTATION_FILE_WARNING_SHOWN] = '1'; + getBuildLogger(userSentryOptions.silent).warn( + `[@sentry/nextjs] Could not find an instrumentation file next to your ${routerDir} directory. Next.js production builds only load it from there, so the Sentry SDK will not be initialized on the server. Create ${path.join(instrumentationDir, 'instrumentation.ts')}, or move your existing instrumentation file there: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#register-server-side-sdk (you can suppress this warning by setting SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING=1 as environment variable)`, + ); +} + /** * Parses the major Next.js version number from a semver string. */ diff --git a/packages/nextjs/test/config/withSentryConfig/getFinalConfigObjectUtils.test.ts b/packages/nextjs/test/config/withSentryConfig/getFinalConfigObjectUtils.test.ts index cfda40eaa28f..273fddb96c01 100644 --- a/packages/nextjs/test/config/withSentryConfig/getFinalConfigObjectUtils.test.ts +++ b/packages/nextjs/test/config/withSentryConfig/getFinalConfigObjectUtils.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { NextConfigObject } from '../../../src/config/types'; +import { warnIfMissingInstrumentationFile } from '../../../src/config/withSentryConfig/getFinalConfigObjectUtils'; import { makeProject, removeProjects } from './projectFixture'; const HANDLER_REWRITE = { source: '/monitoring', destination: '/sentry-tunnel' }; @@ -121,3 +122,93 @@ describe('maybeSetUpTunnelRouteRewriteRules', () => { expect(warnSpy).not.toHaveBeenCalled(); }); }); + +describe('warnIfMissingInstrumentationFile', () => { + const ROOT_FILE_WITH_SRC_APP = ['src/app/page.tsx', 'instrumentation.ts']; + + beforeEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + vi.stubEnv('__SENTRY_MISSING_INSTRUMENTATION_FILE_WARNING_SHOWN__', undefined); + }); + + function setUp(files: string[]) { + vi.spyOn(process, 'cwd').mockReturnValue(makeProject(files)); + return vi.spyOn(console, 'warn').mockImplementation(() => undefined); + } + + function missingFileWarning(routerDir: string, target: string): string { + return `[@sentry/nextjs] Could not find an instrumentation file next to your ${routerDir} directory. Next.js production builds only load it from there, so the Sentry SDK will not be initialized on the server. Create ${target}, or move your existing instrumentation file there: https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/#register-server-side-sdk (you can suppress this warning by setting SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING=1 as environment variable)`; + } + + it.each([ + ['the file is in the project root with `src/app`', ROOT_FILE_WITH_SRC_APP, 'src/app', 'src/instrumentation.ts'], + [ + 'the file is in `src` with `app`', + ['app/page.tsx', 'src/lib/db.ts', 'src/instrumentation.ts'], + 'app', + 'instrumentation.ts', + ], + ['there is no file with `src/pages`', ['src/pages/index.tsx'], 'src/pages', 'src/instrumentation.ts'], + [ + 'the file is next to `src/app`, but a root `pages` directory takes precedence', + ['pages/index.tsx', 'src/app/page.tsx', 'src/instrumentation.ts'], + 'pages', + 'instrumentation.ts', + ], + ])('warns when %s', (_setup, files, routerDir, target) => { + const warnSpy = setUp(files); + + warnIfMissingInstrumentationFile({}, {}); + + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(warnSpy).toHaveBeenCalledWith(missingFileWarning(routerDir, target)); + }); + + it('warns only once', () => { + const warnSpy = setUp(ROOT_FILE_WITH_SRC_APP); + + warnIfMissingInstrumentationFile({}, {}); + warnIfMissingInstrumentationFile({}, {}); + + expect(warnSpy).toHaveBeenCalledTimes(1); + }); + + it.each([ + ['the file is next to `src/app`', ['src/app/page.tsx', 'src/instrumentation.ts']], + ['the file is next to `app`', ['app/page.tsx', 'instrumentation.js']], + ['a file is in both folders', ['src/app/page.tsx', 'src/instrumentation.ts', 'instrumentation.ts']], + ['the project has no `app` or `pages` directory', []], + ])('does not warn when %s', (_setup, files) => { + const warnSpy = setUp(files); + + warnIfMissingInstrumentationFile({}, {}); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('looks for the file with the configured `pageExtensions`', () => { + const warnSpy = setUp(['src/app/page.page.tsx', 'src/instrumentation.page.ts']); + + warnIfMissingInstrumentationFile({ pageExtensions: ['page.tsx', 'page.ts'] }, {}); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('does not warn with `silent`', () => { + const warnSpy = setUp(ROOT_FILE_WITH_SRC_APP); + + warnIfMissingInstrumentationFile({}, { silent: true }); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('does not warn when SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING is set', () => { + vi.stubEnv('SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING', '1'); + const warnSpy = setUp(ROOT_FILE_WITH_SRC_APP); + + warnIfMissingInstrumentationFile({}, {}); + + expect(warnSpy).not.toHaveBeenCalled(); + }); +});