Skip to content

Commit a0c04a7

Browse files
authored
fix(nextjs): Inject Next.js version for dev symbolication (#17379)
1 parent 9b65733 commit a0c04a7

9 files changed

+1316
-39
lines changed

packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ type OriginalStackFrameResponse = {
1212

1313
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
1414
_sentryBasePath?: string;
15-
next?: {
16-
version?: string;
17-
};
15+
_sentryNextJsVersion: string | undefined;
1816
};
1917

2018
/**
@@ -39,9 +37,15 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
3937
try {
4038
if (hint.originalException && hint.originalException instanceof Error && hint.originalException.stack) {
4139
const frames = stackTraceParser.parse(hint.originalException.stack);
40+
const nextJsVersion = globalWithInjectedValues._sentryNextJsVersion;
41+
42+
// If we for whatever reason don't have a Next.js version,
43+
// we don't want to symbolicate as this previously lead to infinite loops
44+
if (!nextJsVersion) {
45+
return event;
46+
}
4247

43-
const nextjsVersion = globalWithInjectedValues.next?.version || '0.0.0';
44-
const parsedNextjsVersion = nextjsVersion ? parseSemver(nextjsVersion) : {};
48+
const parsedNextjsVersion = parseSemver(nextJsVersion);
4549

4650
let resolvedFrames: ({
4751
originalCodeFrame: string | null;
@@ -83,7 +87,9 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
8387
context_line: contextLine,
8488
post_context: postContextLines,
8589
function: resolvedFrame.originalStackFrame.methodName,
86-
filename: resolvedFrame.originalStackFrame.file || undefined,
90+
filename: resolvedFrame.originalStackFrame.file
91+
? stripWebpackInternalPrefix(resolvedFrame.originalStackFrame.file)
92+
: undefined,
8793
lineno:
8894
resolvedFrame.originalStackFrame.lineNumber || resolvedFrame.originalStackFrame.line1 || undefined,
8995
colno: resolvedFrame.originalStackFrame.column || resolvedFrame.originalStackFrame.column1 || undefined,
@@ -281,3 +287,21 @@ function parseOriginalCodeFrame(codeFrame: string): {
281287
postContextLines,
282288
};
283289
}
290+
291+
/**
292+
* Strips webpack-internal prefixes from filenames to clean up stack traces.
293+
*
294+
* Examples:
295+
* - "webpack-internal:///./components/file.tsx" -> "./components/file.tsx"
296+
* - "webpack-internal:///(app-pages-browser)/./components/file.tsx" -> "./components/file.tsx"
297+
*/
298+
function stripWebpackInternalPrefix(filename: string): string | undefined {
299+
if (!filename) {
300+
return filename;
301+
}
302+
303+
const webpackInternalRegex = /^webpack-internal:(?:\/+)?(?:\([^)]*\)\/)?(.+)$/;
304+
const match = filename.match(webpackInternalRegex);
305+
306+
return match ? match[1] : filename;
307+
}

packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { debug } from '@sentry/core';
22
import * as chalk from 'chalk';
3-
import * as path from 'path';
43
import type { RouteManifest } from '../manifest/types';
5-
import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShortcut } from '../types';
4+
import type { NextConfigObject, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
5+
import { generateValueInjectionRules } from './generateValueInjectionRules';
66

77
/**
88
* Construct a Turbopack config object from a Next.js config object and a Turbopack options object.
@@ -14,30 +14,23 @@ import type { NextConfigObject, TurbopackOptions, TurbopackRuleConfigItemOrShort
1414
export function constructTurbopackConfig({
1515
userNextConfig,
1616
routeManifest,
17+
nextJsVersion,
1718
}: {
1819
userNextConfig: NextConfigObject;
1920
routeManifest?: RouteManifest;
21+
nextJsVersion?: string;
2022
}): TurbopackOptions {
2123
const newConfig: TurbopackOptions = {
2224
...userNextConfig.turbopack,
2325
};
2426

25-
if (routeManifest) {
26-
newConfig.rules = safelyAddTurbopackRule(newConfig.rules, {
27-
matcher: '**/instrumentation-client.*',
28-
rule: {
29-
loaders: [
30-
{
31-
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
32-
options: {
33-
values: {
34-
_sentryRouteManifest: JSON.stringify(routeManifest),
35-
},
36-
},
37-
},
38-
],
39-
},
40-
});
27+
const valueInjectionRules = generateValueInjectionRules({
28+
routeManifest,
29+
nextJsVersion,
30+
});
31+
32+
for (const { matcher, rule } of valueInjectionRules) {
33+
newConfig.rules = safelyAddTurbopackRule(newConfig.rules, { matcher, rule });
4134
}
4235

4336
return newConfig;
@@ -53,7 +46,7 @@ export function constructTurbopackConfig({
5346
*/
5447
export function safelyAddTurbopackRule(
5548
existingRules: TurbopackOptions['rules'],
56-
{ matcher, rule }: { matcher: string; rule: TurbopackRuleConfigItemOrShortcut },
49+
{ matcher, rule }: TurbopackMatcherWithRule,
5750
): TurbopackOptions['rules'] {
5851
if (!existingRules) {
5952
return {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as path from 'path';
2+
import type { RouteManifest } from '../manifest/types';
3+
import type { JSONValue, TurbopackMatcherWithRule } from '../types';
4+
5+
/**
6+
* Generate the value injection rules for client and server in turbopack config.
7+
*/
8+
export function generateValueInjectionRules({
9+
routeManifest,
10+
nextJsVersion,
11+
}: {
12+
routeManifest?: RouteManifest;
13+
nextJsVersion?: string;
14+
}): TurbopackMatcherWithRule[] {
15+
const rules: TurbopackMatcherWithRule[] = [];
16+
const isomorphicValues: Record<string, JSONValue> = {};
17+
let clientValues: Record<string, JSONValue> = {};
18+
let serverValues: Record<string, JSONValue> = {};
19+
20+
if (nextJsVersion) {
21+
// This is used to determine version-based dev-symbolication behavior
22+
isomorphicValues._sentryNextJsVersion = nextJsVersion;
23+
}
24+
25+
if (routeManifest) {
26+
clientValues._sentryRouteManifest = JSON.stringify(routeManifest);
27+
}
28+
29+
if (Object.keys(isomorphicValues).length > 0) {
30+
clientValues = { ...clientValues, ...isomorphicValues };
31+
serverValues = { ...serverValues, ...isomorphicValues };
32+
}
33+
34+
// Client value injection
35+
if (Object.keys(clientValues).length > 0) {
36+
rules.push({
37+
matcher: '**/instrumentation-client.*',
38+
rule: {
39+
loaders: [
40+
{
41+
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
42+
options: {
43+
values: clientValues,
44+
},
45+
},
46+
],
47+
},
48+
});
49+
}
50+
51+
// Server value injection
52+
if (Object.keys(serverValues).length > 0) {
53+
rules.push({
54+
matcher: '**/instrumentation.*',
55+
rule: {
56+
loaders: [
57+
{
58+
loader: path.resolve(__dirname, '..', 'loaders', 'valueInjectionLoader.js'),
59+
options: {
60+
values: serverValues,
61+
},
62+
},
63+
],
64+
},
65+
});
66+
}
67+
68+
return rules;
69+
}

packages/nextjs/src/config/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ export type EnhancedGlobal = typeof GLOBAL_OBJ & {
621621
SENTRY_RELEASES?: { [key: string]: { id: string } };
622622
};
623623

624-
type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
624+
export type JSONValue = string | number | boolean | JSONValue[] | { [k: string]: JSONValue };
625625

626626
type TurbopackLoaderItem =
627627
| string
@@ -637,6 +637,11 @@ type TurbopackRuleCondition = {
637637

638638
export type TurbopackRuleConfigItemOrShortcut = TurbopackLoaderItem[] | TurbopackRuleConfigItem;
639639

640+
export type TurbopackMatcherWithRule = {
641+
matcher: string;
642+
rule: TurbopackRuleConfigItemOrShortcut;
643+
};
644+
640645
type TurbopackRuleConfigItemOptions = {
641646
loaders: TurbopackLoaderItem[];
642647
as?: string;

packages/nextjs/src/config/webpack.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function constructWebpackConfigFunction(
4545
userSentryOptions: SentryBuildOptions = {},
4646
releaseName: string | undefined,
4747
routeManifest: RouteManifest | undefined,
48+
nextJsVersion: string | undefined,
4849
): WebpackConfigFunction {
4950
// Will be called by nextjs and passed its default webpack configuration and context data about the build (whether
5051
// we're building server or client, whether we're in dev, what version of webpack we're using, etc). Note that
@@ -90,7 +91,15 @@ export function constructWebpackConfigFunction(
9091
const newConfig = setUpModuleRules(rawNewConfig);
9192

9293
// Add a loader which will inject code that sets global values
93-
addValueInjectionLoader(newConfig, userNextConfig, userSentryOptions, buildContext, releaseName, routeManifest);
94+
addValueInjectionLoader({
95+
newConfig,
96+
userNextConfig,
97+
userSentryOptions,
98+
buildContext,
99+
releaseName,
100+
routeManifest,
101+
nextJsVersion,
102+
});
94103

95104
addOtelWarningIgnoreRule(newConfig);
96105

@@ -682,14 +691,23 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
682691
*/
683692
// 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.
684693
// 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.
685-
function addValueInjectionLoader(
686-
newConfig: WebpackConfigObjectWithModuleRules,
687-
userNextConfig: NextConfigObject,
688-
userSentryOptions: SentryBuildOptions,
689-
buildContext: BuildContext,
690-
releaseName: string | undefined,
691-
routeManifest: RouteManifest | undefined,
692-
): void {
694+
function addValueInjectionLoader({
695+
newConfig,
696+
userNextConfig,
697+
userSentryOptions,
698+
buildContext,
699+
releaseName,
700+
routeManifest,
701+
nextJsVersion,
702+
}: {
703+
newConfig: WebpackConfigObjectWithModuleRules;
704+
userNextConfig: NextConfigObject;
705+
userSentryOptions: SentryBuildOptions;
706+
buildContext: BuildContext;
707+
releaseName: string | undefined;
708+
routeManifest: RouteManifest | undefined;
709+
nextJsVersion: string | undefined;
710+
}): void {
693711
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
694712

695713
// Check if release creation is disabled to prevent injection that breaks build determinism
@@ -710,6 +728,8 @@ function addValueInjectionLoader(
710728
// Only inject if release creation is not explicitly disabled (to maintain build determinism)
711729
SENTRY_RELEASE: releaseToInject && !buildContext.dev ? { id: releaseToInject } : undefined,
712730
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
731+
// This is used to determine version-based dev-symbolication behavior
732+
_sentryNextJsVersion: nextJsVersion,
713733
};
714734

715735
const serverValues = {

packages/nextjs/src/config/withSentryConfig.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,19 @@ function getFinalConfigObject(
314314
webpack:
315315
isTurbopack || userSentryOptions.disableSentryWebpackConfig
316316
? incomingUserNextConfigObject.webpack // just return the original webpack config
317-
: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName, routeManifest),
317+
: constructWebpackConfigFunction(
318+
incomingUserNextConfigObject,
319+
userSentryOptions,
320+
releaseName,
321+
routeManifest,
322+
nextJsVersion,
323+
),
318324
...(isTurbopackSupported && isTurbopack
319325
? {
320326
turbopack: constructTurbopackConfig({
321327
userNextConfig: incomingUserNextConfigObject,
322328
routeManifest,
329+
nextJsVersion,
323330
}),
324331
}
325332
: {}),

0 commit comments

Comments
 (0)