Skip to content

fix(nextjs): Consider pageExtensions when looking for instrumentation file #15701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 17, 2025
Merged
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
94 changes: 39 additions & 55 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,25 @@ export function constructWebpackConfigFunction(
): WebpackConfigObject {
const { isServer, dev: isDev, dir: projectDir } = buildContext;
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'];
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');

// We add `.ts` and `.js` back in because `pageExtensions` might not be relevant to the instrumentation file
// e.g. user's setting `.mdx`. In that case we still want to default look up
// `instrumentation.ts` and `instrumentation.js`
const instrumentationFile = getInstrumentationFile(projectDir, dotPrefixedPageExtensions.concat(['.ts', '.js']));

if (runtime !== 'client') {
warnAboutDeprecatedConfigFiles(projectDir, runtime);
warnAboutDeprecatedConfigFiles(projectDir, instrumentationFile, runtime);
}
if (runtime === 'server') {
const nextJsVersion = getNextjsVersion();
const { major } = parseSemver(nextJsVersion || '');
// was added in v15 (https://github.com/vercel/next.js/pull/67539)
if (major && major >= 15) {
warnAboutMissingOnRequestErrorHandler(projectDir);
warnAboutMissingOnRequestErrorHandler(instrumentationFile);
}
}

Expand Down Expand Up @@ -110,11 +119,6 @@ export function constructWebpackConfigFunction(
? path.join(appDirPath, '..')
: projectDir;

// 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'];
const dotPrefixedPageExtensions = pageExtensions.map(ext => `.${ext}`);
const pageExtensionRegex = pageExtensions.map(escapeStringForRegex).join('|');

const staticWrappingLoaderOptions = {
appDir: appDirPath,
pagesDir: pagesDirPath,
Expand Down Expand Up @@ -445,37 +449,29 @@ async function addSentryToClientEntryProperty(
}

/**
* Make sure the instrumentation file has a `onRequestError` Handler
*
* @param projectDir The root directory of the project, where config files would be located
* Gets the content of the user's instrumentation file
*/
function warnAboutMissingOnRequestErrorHandler(projectDir: string): void {
const instrumentationPaths = [
['src', 'instrumentation.ts'],
['src', 'instrumentation.js'],
['instrumentation.ts'],
['instrumentation.js'],
];
const instrumentationFile = instrumentationPaths
.map(pathSegments => path.resolve(projectDir, ...pathSegments))
.find(function exists(filePath: string): string | null {
try {
fs.accessSync(filePath, fs.constants.F_OK);
return filePath;
} catch (error) {
return null;
}
});
function getInstrumentationFile(projectDir: string, dotPrefixedExtensions: string[]): string | null {
const paths = dotPrefixedExtensions.flatMap(extension => [
['src', `instrumentation${extension}`],
[`instrumentation${extension}`],
]);

function hasOnRequestErrorHandler(absolutePath: string): boolean {
for (const pathSegments of paths) {
try {
const content = fs.readFileSync(absolutePath, 'utf8');
return content.includes('onRequestError');
} catch (error) {
return false;
return fs.readFileSync(path.resolve(projectDir, ...pathSegments), { encoding: 'utf-8' });
} catch (e) {
// no-op
}
}

return null;
}

/**
* Make sure the instrumentation file has a `onRequestError` Handler
*/
function warnAboutMissingOnRequestErrorHandler(instrumentationFile: string | null): void {
if (!instrumentationFile) {
if (!process.env.SENTRY_SUPPRESS_INSTRUMENTATION_FILE_WARNING) {
// eslint-disable-next-line no-console
Expand All @@ -488,7 +484,7 @@ function warnAboutMissingOnRequestErrorHandler(projectDir: string): void {
return;
}

if (!hasOnRequestErrorHandler(instrumentationFile)) {
if (!instrumentationFile.includes('onRequestError')) {
// eslint-disable-next-line no-console
console.warn(
chalk.yellow(
Expand All @@ -505,27 +501,15 @@ function warnAboutMissingOnRequestErrorHandler(projectDir: string): void {
* @param projectDir The root directory of the project, where config files would be located
* @param platform Either "server" or "edge", so that we know which file to look for
*/
function warnAboutDeprecatedConfigFiles(projectDir: string, platform: 'server' | 'edge'): void {
const hasInstrumentationHookWithIndicationsOfSentry = [
['src', 'instrumentation.ts'],
['src', 'instrumentation.js'],
['instrumentation.ts'],
['instrumentation.js'],
].some(potentialInstrumentationHookPathSegments => {
try {
const instrumentationHookContent = fs.readFileSync(
path.resolve(projectDir, ...potentialInstrumentationHookPathSegments),
{ encoding: 'utf-8' },
);

return (
instrumentationHookContent.includes('@sentry/') ||
instrumentationHookContent.match(/sentry\.(server|edge)\.config(\.(ts|js))?/)
);
} catch (e) {
return false;
}
});
function warnAboutDeprecatedConfigFiles(
projectDir: string,
instrumentationFile: string | null,
platform: 'server' | 'edge',
): void {
const hasInstrumentationHookWithIndicationsOfSentry =
instrumentationFile &&
(instrumentationFile.includes('@sentry/') ||
instrumentationFile.match(/sentry\.(server|edge)\.config(\.(ts|js))?/));

if (hasInstrumentationHookWithIndicationsOfSentry) {
return;
Expand All @@ -535,7 +519,7 @@ function warnAboutDeprecatedConfigFiles(projectDir: string, platform: 'server' |
if (fs.existsSync(path.resolve(projectDir, filename))) {
// eslint-disable-next-line no-console
console.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 hook instead. To ensure correct functionality of the SDK, \`Sentry.init\` must be called inside \`instrumentation.ts\`. Learn more about setting up an instrumentation hook in Next.js: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation. You can safely delete the \`${filename}\` file afterward.`,
`[@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.`,
);
}
}
Expand Down
Loading