Skip to content

fix(nextjs): Inject Next.js version for dev symbolication #17379

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 7 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
36 changes: 30 additions & 6 deletions packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ type OriginalStackFrameResponse = {

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
_sentryBasePath?: string;
next?: {
version?: string;
};
_sentryNextJsVersion: string | undefined;
};

/**
Expand All @@ -39,9 +37,15 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
try {
if (hint.originalException && hint.originalException instanceof Error && hint.originalException.stack) {
const frames = stackTraceParser.parse(hint.originalException.stack);
const nextJsVersion = globalWithInjectedValues._sentryNextJsVersion;

// If we for whatever reason don't have a Next.js version,
// we don't want to symbolicate as this previously lead to infinite loops
if (!nextJsVersion) {
return event;
}

const nextjsVersion = globalWithInjectedValues.next?.version || '0.0.0';
const parsedNextjsVersion = nextjsVersion ? parseSemver(nextjsVersion) : {};
const parsedNextjsVersion = parseSemver(nextJsVersion);

let resolvedFrames: ({
originalCodeFrame: string | null;
Expand Down Expand Up @@ -83,7 +87,9 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
context_line: contextLine,
post_context: postContextLines,
function: resolvedFrame.originalStackFrame.methodName,
filename: resolvedFrame.originalStackFrame.file || undefined,
filename: resolvedFrame.originalStackFrame.file
? stripWebpackInternalPrefix(resolvedFrame.originalStackFrame.file)
: undefined,
lineno:
resolvedFrame.originalStackFrame.lineNumber || resolvedFrame.originalStackFrame.line1 || undefined,
colno: resolvedFrame.originalStackFrame.column || resolvedFrame.originalStackFrame.column1 || undefined,
Expand Down Expand Up @@ -281,3 +287,21 @@ function parseOriginalCodeFrame(codeFrame: string): {
postContextLines,
};
}

/**
* Strips webpack-internal prefixes from filenames to clean up stack traces.
*
* Examples:
* - "webpack-internal:///./components/file.tsx" -> "./components/file.tsx"
* - "webpack-internal:///(app-pages-browser)/./components/file.tsx" -> "./components/file.tsx"
*/
function stripWebpackInternalPrefix(filename: string): string | undefined {
if (!filename) {
return filename;
}

const webpackInternalRegex = /^webpack-internal:(?:\/+)?(?:\([^)]*\)\/)?(.+)$/;
const match = filename.match(webpackInternalRegex);

return match ? match[1] : filename;
}
31 changes: 12 additions & 19 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { debug } from '@sentry/core';
import * as chalk from 'chalk';
import * as path from 'path';
import type { RouteManifest } from '../manifest/types';
import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShortcut } from '../types';
import type { NextConfigObject, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
import { generateValueInjectionRules } from './generateValueInjectionRules';

/**
* Construct a Turbopack config object from a Next.js config object and a Turbopack options object.
Expand All @@ -14,30 +14,23 @@ import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShort
export function constructTurbopackConfig({
userNextConfig,
routeManifest,
nextJsVersion,
}: {
userNextConfig: NextConfigObject;
routeManifest?: RouteManifest;
nextJsVersion?: string;
}): TurbopackOptions {
const newConfig: TurbopackOptions = {
...userNextConfig.turbopack,
};

if (routeManifest) {
newConfig.rules = safelyAddTurbopackRule(newConfig.rules, {
matcher: '**/instrumentation-client.*',
rule: {
loaders: [
{
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
options: {
values: {
_sentryRouteManifest: JSON.stringify(routeManifest),
},
},
},
],
},
});
const valueInjectionRules = generateValueInjectionRules({
routeManifest,
nextJsVersion,
});

for (const { matcher, rule } of valueInjectionRules) {
newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule });
}

return newConfig;
Expand All @@ -53,7 +46,7 @@ export function constructTurbopackConfig({
*/
export function safelyAddTurbopackRule(
existingRules: TurbopackOptions['rules'],
{ matcher, rule }: { matcher: string; rule: TurbopackRuleConfigItemOrShortcut },
{ matcher, rule }: TurbopackMatcherWithRule,
): TurbopackOptions['rules'] {
if (!existingRules) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as path from 'path';
import type { RouteManifest } from '../manifest/types';
import type { JSONValue, TurbopackMatcherWithRule } from '../types';

/**
* Generate the value injection rules for client and server in turbopack config.
*/
export function generateValueInjectionRules({
routeManifest,
nextJsVersion,
}: {
routeManifest?: RouteManifest;
nextJsVersion?: string;
}): TurbopackMatcherWithRule[] {
const rules: TurbopackMatcherWithRule[] = [];
const isomorphicValues: Record<string, JSONValue> = {};
let clientValues: Record<string, JSONValue> = {};
let serverValues: Record<string, JSONValue> = {};

if (nextJsVersion) {
isomorphicValues._sentryNextJsVersion = nextJsVersion;
}

if (routeManifest) {
clientValues._sentryRouteManifest = JSON.stringify(routeManifest);
}

if (Object.keys(isomorphicValues).length > 0) {
clientValues = { ...clientValues, ...isomorphicValues };
serverValues = { ...serverValues, ...isomorphicValues };
}

// Client value injection
if (Object.keys(clientValues).length > 0) {
rules.push({
matcher: '**/instrumentation-client.*',
rule: {
loaders: [
{
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
options: {
values: clientValues,
},
},
],
},
});
}

// Server value injection
if (Object.keys(serverValues).length > 0) {
rules.push({
matcher: '**/instrumentation.*',
rule: {
loaders: [
{
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
options: {
values: serverValues,
},
},
],
},
});
}

return rules;
}
7 changes: 6 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ export type EnhancedGlobal = typeof GLOBAL_OBJ & {
SENTRY_RELEASES?: { [key: string]: { id: string } };
};

type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
export type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };

type TurbopackLoaderItem =
| string
Expand All @@ -637,6 +637,11 @@ type TurbopackRuleCondition = {

export type TurbopackRuleConfigItemOrShortcut = TurbopackLoaderItem[] | TurbopackRuleConfigItem;

export type TurbopackMatcherWithRule = {
matcher: string;
rule: TurbopackRuleConfigItemOrShortcut;
};

type TurbopackRuleConfigItemOptions = {
loaders: TurbopackLoaderItem[];
as?: string;
Expand Down
37 changes: 28 additions & 9 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function constructWebpackConfigFunction(
userSentryOptions: SentryBuildOptions = {},
releaseName: string | undefined,
routeManifest: RouteManifest | undefined,
nextJsVersion: string | undefined,
): WebpackConfigFunction {
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
Expand Down Expand Up @@ -90,7 +91,15 @@ export function constructWebpackConfigFunction(
const newConfig = setUpModuleRules(rawNewConfig);

// Add a loader which will inject code that sets global values
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, releaseName, routeManifest);
addValueInjectionLoader({
newConfig,
userNextConfig,
userSentryOptions,
buildContext,
releaseName,
routeManifest,
nextJsVersion,
});

addOtelWarningIgnoreRule(newConfig);

Expand Down Expand Up @@ -682,14 +691,23 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
*/
// TODO: Remove this loader and replace it with a nextConfig.env (https://web.archive.org/web/20240917153554/https://nextjs.org/docs/app/api-reference/next-config-js/env) or define based (https://github.com/vercel/next.js/discussions/71476) approach.
// In order to remove this loader though we need to make sure the minimum supported Next.js version includes this PR (https://github.com/vercel/next.js/pull/61194), otherwise the nextConfig.env based approach will not work, as our SDK code is not processed by Next.js.
function addValueInjectionLoader(
newConfig: WebpackConfigObjectWithModuleRules,
userNextConfig: NextConfigObject,
userSentryOptions: SentryBuildOptions,
buildContext: BuildContext,
releaseName: string | undefined,
routeManifest: RouteManifest | undefined,
): void {
function addValueInjectionLoader({
newConfig,
userNextConfig,
userSentryOptions,
buildContext,
releaseName,
routeManifest,
nextJsVersion,
}: {
newConfig: WebpackConfigObjectWithModuleRules;
userNextConfig: NextConfigObject;
userSentryOptions: SentryBuildOptions;
buildContext: BuildContext;
releaseName: string | undefined;
routeManifest: RouteManifest | undefined;
nextJsVersion: string | undefined;
}): void {
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';

// Check if release creation is disabled to prevent injection that breaks build determinism
Expand All @@ -710,6 +728,7 @@ function addValueInjectionLoader(
// Only inject if release creation is not explicitly disabled (to maintain build determinism)
SENTRY_RELEASE: releaseToInject && !buildContext.dev ? { id: releaseToInject } : undefined,
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
_sentryNextJsVersion: nextJsVersion,
};

const serverValues = {
Expand Down
9 changes: 8 additions & 1 deletion packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,12 +314,19 @@ function getFinalConfigObject(
webpack:
isTurbopack || userSentryOptions.disableSentryWebpackConfig
? incomingUserNextConfigObject.webpack // just return the original webpack config
: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName, routeManifest),
: constructWebpackConfigFunction(
incomingUserNextConfigObject,
userSentryOptions,
releaseName,
routeManifest,
nextJsVersion,
),
...(isTurbopackSupported && isTurbopack
? {
turbopack: constructTurbopackConfig({
userNextConfig: incomingUserNextConfigObject,
routeManifest,
nextJsVersion,
}),
}
: {}),
Expand Down
Loading