Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
maybeSetUpTunnelRouteRewriteRules,
resolveReleaseName,
shouldReturnEarlyInExperimentalBuildMode,
warnIfMisplacedInstrumentationFile,
warnIfMissingOnRouterTransitionStartHook,
} from './getFinalConfigObjectUtils';

Expand Down Expand Up @@ -55,6 +56,7 @@ export function getFinalConfigObject(
maybeSetClientTraceMetadataOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent);
maybeSetInstrumentationHookOption(incomingUserNextConfigObject, nextJsVersion, userSentryOptions.silent);
warnIfMissingOnRouterTransitionStartHook(userSentryOptions);
warnIfMisplacedInstrumentationFile(incomingUserNextConfigObject, userSentryOptions);

const bundlerInfo = getBundlerInfo(nextJsVersion);
maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo, userSentryOptions.silent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +21,8 @@ let showedUnusedTunnelRouteHandlerWarning = false;
let showedTunnelRouteHandlerModeLog = false;
let showedExperimentalBuildModeWarning = false;

const MISPLACED_INSTRUMENTATION_FILE_WARNING_SHOWN = '__SENTRY_MISPLACED_INSTRUMENTATION_FILE_WARNING_SHOWN__';

/**
* Resolves the Sentry release name to use for build-time behavior.
*
Expand Down Expand Up @@ -266,6 +268,48 @@ export function warnIfMissingOnRouterTransitionStartHook(userSentryOptions: Sent
}
}

/**
* Warns if the `instrumentation` file is not in the folder that contains the `app` or `pages` directory.
* `next build` only looks there, even though dev and Turbopack also find the file in other places.
*/
export function warnIfMisplacedInstrumentationFile(
incomingUserNextConfigObject: NextConfigObject,
userSentryOptions: SentryBuildOptions,
): void {
if (
process.env[MISPLACED_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 lookup as Next.js' `findPagesDir`: `pages` before `app`, and `<name>` before `src/<name>`.
const routerDir = ['pages', 'app'].flatMap(name => [name, path.join('src', name)]).find(exists);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const routerDir = ['pages', 'app'].flatMap(name => [name, path.join('src', name)]).find(exists);
const routerDir = ['pages', path.join('src', 'pages'), 'app', path.join('src', 'app')].find(exists);

just a suggestion for readability, imo this is a bit more explicit and easier to understand at first glance

if (!routerDir) {
return;
}

const loadedDir = path.dirname(routerDir);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to show the user in what wrong location the instrumentation file is? as in could we not just simplify this logic to:

  1. get the path where we expect the instrumentation file to live
  2. check whether its there
  3. if not log something like "Could not find a server instrumentation file next to {router_dir}. Create {router_dir}/instrumentation.{extension}, or move your existing instrumentation file there."

I'd assume a user will know where he placed this file or can easily find out and less logic that might be buggy

const ignoredDir = loadedDir === 'src' ? '.' : 'src';
const pageExtensions = incomingUserNextConfigObject.pageExtensions ?? DEFAULT_PAGE_EXTENSIONS;
const findInstrumentationFile = (dir: string): string | undefined =>
pageExtensions.map(extension => path.join(dir, `instrumentation.${extension}`)).find(exists);

const misplacedFile = findInstrumentationFile(ignoredDir);
if (!misplacedFile || findInstrumentationFile(loadedDir)) {
return;
}

// Next.js may evaluate its config in child processes, which inherit this state from their parent.
process.env[MISPLACED_INSTRUMENTATION_FILE_WARNING_SHOWN] = '1';
getBuildLogger(userSentryOptions.silent).warn(
`[@sentry/nextjs] Found ${misplacedFile}, but Next.js production builds only pick up the instrumentation file next to your ${routerDir} directory. Move it to ${path.join(loadedDir, path.basename(misplacedFile))}, or the Sentry SDK may not be initialized on the server in production.`,
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feat lacks integration or E2E test

Low Severity

This feat is covered only by unit tests that call warnIfMisplacedInstrumentationFile against a temp fixture. This is flagged because the review rules ask feat PRs to include at least one integration or E2E test, for example asserting that next build actually emits the warning for a misplaced instrumentation.ts.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 26e519d. Configure here.


/**
* Parses the major Next.js version number from a semver string.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { NextConfigObject } from '../../../src/config/types';
import { warnIfMisplacedInstrumentationFile } from '../../../src/config/withSentryConfig/getFinalConfigObjectUtils';
import { makeProject, removeProjects } from './projectFixture';

const HANDLER_REWRITE = { source: '/monitoring', destination: '/sentry-tunnel' };
Expand Down Expand Up @@ -121,3 +122,104 @@ describe('maybeSetUpTunnelRouteRewriteRules', () => {
expect(warnSpy).not.toHaveBeenCalled();
});
});

describe('warnIfMisplacedInstrumentationFile', () => {
const ROOT_FILE_WITH_SRC_APP = ['src/app/page.tsx', 'instrumentation.ts'];

beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
vi.stubEnv('__SENTRY_MISPLACED_INSTRUMENTATION_FILE_WARNING_SHOWN__', undefined);
});

function setUp(files: string[]) {
vi.spyOn(process, 'cwd').mockReturnValue(makeProject(files));
return vi.spyOn(console, 'warn').mockImplementation(() => undefined);
}

function misplacedFileWarning(found: string, routerDir: string, target: string): string {
return `[@sentry/nextjs] Found ${found}, but Next.js production builds only pick up the instrumentation file next to your ${routerDir} directory. Move it to ${target}, or the Sentry SDK may not be initialized on the server in production.`;
}

it.each([
{
found: 'instrumentation.ts',
routerDir: 'src/app',
target: 'src/instrumentation.ts',
files: ROOT_FILE_WITH_SRC_APP,
},
{
found: 'instrumentation.js',
routerDir: 'src/pages',
target: 'src/instrumentation.js',
files: ['src/pages/index.tsx', 'instrumentation.js'],
},
{
found: 'src/instrumentation.ts',
routerDir: 'app',
target: 'instrumentation.ts',
files: ['app/page.tsx', 'src/lib/db.ts', 'src/instrumentation.ts'],
},
])('warns about $found in a project with $routerDir', ({ found, routerDir, target, files }) => {
const warnSpy = setUp(files);

warnIfMisplacedInstrumentationFile({}, {});

expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(misplacedFileWarning(found, routerDir, target));
});

it('looks for the file with the configured `pageExtensions`', () => {
const warnSpy = setUp(['src/app/page.page.tsx', 'instrumentation.page.ts']);

warnIfMisplacedInstrumentationFile({ pageExtensions: ['page.tsx', 'page.ts'] }, {});

expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
misplacedFileWarning('instrumentation.page.ts', 'src/app', 'src/instrumentation.page.ts'),
);
});

it('warns only once', () => {
const warnSpy = setUp(ROOT_FILE_WITH_SRC_APP);

warnIfMisplacedInstrumentationFile({}, {});
warnIfMisplacedInstrumentationFile({}, {});

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.ts']],
['a file is in both folders', ['src/app/page.tsx', 'src/instrumentation.ts', 'instrumentation.ts']],
[
'a root `pages` directory takes precedence over `src/app`',
['pages/index.tsx', 'src/app/page.tsx', 'instrumentation.ts'],
],
['the project has no `app` or `pages` directory', ['instrumentation.ts']],
])('does not warn when %s', (_setup, files) => {
const warnSpy = setUp(files);

warnIfMisplacedInstrumentationFile({}, {});

expect(warnSpy).not.toHaveBeenCalled();
});

it('does not warn with `silent`', () => {
const warnSpy = setUp(ROOT_FILE_WITH_SRC_APP);

warnIfMisplacedInstrumentationFile({}, { 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);

warnIfMisplacedInstrumentationFile({}, {});

expect(warnSpy).not.toHaveBeenCalled();
});
});
Loading