Skip to content

Commit b90e863

Browse files
chargomeandreiborza
authored andcommitted
feat(nextjs): Support native debugIds in turbopack (getsentry#17853)
Adds support for vercel/next.js#84319 - Switches to automatically injecting native debug Ids whenever the Next.js version supports it - Updates core functionality on supporting `sentryDebugId` alongside the more generic `debugId` that Vercel uses. - Something to consider: We write both `sentryDebugIds` and `debugIds` into the cache but since we generate them in this order, `debugIds` will have precedence when there is a bundle with both keys in it. closes getsentry#17841
1 parent a0de13f commit b90e863

File tree

11 files changed

+409
-41
lines changed

11 files changed

+409
-41
lines changed

.size-limit.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ module.exports = [
103103
path: 'packages/browser/build/npm/esm/index.js',
104104
import: createImport('init', 'feedbackAsyncIntegration'),
105105
gzip: true,
106-
limit: '34 KB',
106+
limit: '35 KB',
107107
},
108108
// React SDK (ESM)
109109
{
@@ -215,7 +215,7 @@ module.exports = [
215215
import: createImport('init'),
216216
ignore: ['$app/stores'],
217217
gzip: true,
218-
limit: '41 KB',
218+
limit: '42 KB',
219219
},
220220
// Node-Core SDK (ESM)
221221
{

packages/core/src/utils/debug-ids.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,56 +6,82 @@ type StackString = string;
66
type CachedResult = [string, string];
77

88
let parsedStackResults: Record<StackString, CachedResult> | undefined;
9-
let lastKeysCount: number | undefined;
9+
let lastSentryKeysCount: number | undefined;
10+
let lastNativeKeysCount: number | undefined;
1011
let cachedFilenameDebugIds: Record<string, string> | undefined;
1112

1213
/**
1314
* Returns a map of filenames to debug identifiers.
15+
* Supports both proprietary _sentryDebugIds and native _debugIds (e.g., from Vercel) formats.
1416
*/
1517
export function getFilenameToDebugIdMap(stackParser: StackParser): Record<string, string> {
16-
const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
17-
if (!debugIdMap) {
18+
const sentryDebugIdMap = GLOBAL_OBJ._sentryDebugIds;
19+
const nativeDebugIdMap = GLOBAL_OBJ._debugIds;
20+
21+
if (!sentryDebugIdMap && !nativeDebugIdMap) {
1822
return {};
1923
}
2024

21-
const debugIdKeys = Object.keys(debugIdMap);
25+
const sentryDebugIdKeys = sentryDebugIdMap ? Object.keys(sentryDebugIdMap) : [];
26+
const nativeDebugIdKeys = nativeDebugIdMap ? Object.keys(nativeDebugIdMap) : [];
2227

2328
// If the count of registered globals hasn't changed since the last call, we
2429
// can just return the cached result.
25-
if (cachedFilenameDebugIds && debugIdKeys.length === lastKeysCount) {
30+
if (
31+
cachedFilenameDebugIds &&
32+
sentryDebugIdKeys.length === lastSentryKeysCount &&
33+
nativeDebugIdKeys.length === lastNativeKeysCount
34+
) {
2635
return cachedFilenameDebugIds;
2736
}
2837

29-
lastKeysCount = debugIdKeys.length;
30-
31-
// Build a map of filename -> debug_id.
32-
cachedFilenameDebugIds = debugIdKeys.reduce<Record<string, string>>((acc, stackKey) => {
33-
if (!parsedStackResults) {
34-
parsedStackResults = {};
35-
}
38+
lastSentryKeysCount = sentryDebugIdKeys.length;
39+
lastNativeKeysCount = nativeDebugIdKeys.length;
3640

37-
const result = parsedStackResults[stackKey];
41+
// Build a map of filename -> debug_id from both sources
42+
cachedFilenameDebugIds = {};
3843

39-
if (result) {
40-
acc[result[0]] = result[1];
41-
} else {
42-
const parsedStack = stackParser(stackKey);
43-
44-
for (let i = parsedStack.length - 1; i >= 0; i--) {
45-
const stackFrame = parsedStack[i];
46-
const filename = stackFrame?.filename;
47-
const debugId = debugIdMap[stackKey];
44+
if (!parsedStackResults) {
45+
parsedStackResults = {};
46+
}
4847

49-
if (filename && debugId) {
50-
acc[filename] = debugId;
51-
parsedStackResults[stackKey] = [filename, debugId];
52-
break;
48+
const processDebugIds = (debugIdKeys: string[], debugIdMap: Record<string, string>): void => {
49+
for (const key of debugIdKeys) {
50+
const debugId = debugIdMap[key];
51+
const result = parsedStackResults?.[key];
52+
53+
if (result && cachedFilenameDebugIds && debugId) {
54+
// Use cached filename but update with current debug ID
55+
cachedFilenameDebugIds[result[0]] = debugId;
56+
// Update cached result with new debug ID
57+
if (parsedStackResults) {
58+
parsedStackResults[key] = [result[0], debugId];
59+
}
60+
} else if (debugId) {
61+
const parsedStack = stackParser(key);
62+
63+
for (let i = parsedStack.length - 1; i >= 0; i--) {
64+
const stackFrame = parsedStack[i];
65+
const filename = stackFrame?.filename;
66+
67+
if (filename && cachedFilenameDebugIds && parsedStackResults) {
68+
cachedFilenameDebugIds[filename] = debugId;
69+
parsedStackResults[key] = [filename, debugId];
70+
break;
71+
}
5372
}
5473
}
5574
}
75+
};
76+
77+
if (sentryDebugIdMap) {
78+
processDebugIds(sentryDebugIdKeys, sentryDebugIdMap);
79+
}
5680

57-
return acc;
58-
}, {});
81+
// Native _debugIds will override _sentryDebugIds if same file
82+
if (nativeDebugIdMap) {
83+
processDebugIds(nativeDebugIdKeys, nativeDebugIdMap);
84+
}
5985

6086
return cachedFilenameDebugIds;
6187
}

packages/core/src/utils/worldwide.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ export type InternalGlobal = {
4141
* file.
4242
*/
4343
_sentryDebugIds?: Record<string, string>;
44+
/**
45+
* Native debug IDs implementation (e.g., from Vercel).
46+
* This uses the same format as _sentryDebugIds but with a different global name.
47+
* Keys are `error.stack` strings, values are debug IDs.
48+
*/
49+
_debugIds?: Record<string, string>;
4450
/**
4551
* Raw module metadata that is injected by bundler plugins.
4652
*

packages/core/test/lib/prepareEvent.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { clearGlobalScope } from '../testutils';
1919
describe('applyDebugIds', () => {
2020
afterEach(() => {
2121
GLOBAL_OBJ._sentryDebugIds = undefined;
22+
GLOBAL_OBJ._debugIds = undefined;
2223
});
2324

2425
it("should put debug IDs into an event's stack frames", () => {
@@ -114,6 +115,139 @@ describe('applyDebugIds', () => {
114115
debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
115116
});
116117
});
118+
119+
it('should support native _debugIds format', () => {
120+
GLOBAL_OBJ._debugIds = {
121+
'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
122+
'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
123+
'filename4.js\nfilename4.js': 'cccccccc-cccc-4ccc-cccc-cccccccccc',
124+
};
125+
126+
const stackParser = createStackParser([0, line => ({ filename: line })]);
127+
128+
const event: Event = {
129+
exception: {
130+
values: [
131+
{
132+
stacktrace: {
133+
frames: [
134+
{ filename: 'filename1.js' },
135+
{ filename: 'filename2.js' },
136+
{ filename: 'filename1.js' },
137+
{ filename: 'filename3.js' },
138+
],
139+
},
140+
},
141+
],
142+
},
143+
};
144+
145+
applyDebugIds(event, stackParser);
146+
147+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
148+
filename: 'filename1.js',
149+
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
150+
});
151+
152+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
153+
filename: 'filename2.js',
154+
debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
155+
});
156+
157+
// expect not to contain an image for the stack frame that doesn't have a corresponding debug id
158+
expect(event.exception?.values?.[0]?.stacktrace?.frames).not.toContainEqual(
159+
expect.objectContaining({
160+
filename3: 'filename3.js',
161+
debug_id: expect.any(String),
162+
}),
163+
);
164+
});
165+
166+
it('should merge both _sentryDebugIds and _debugIds when both exist', () => {
167+
GLOBAL_OBJ._sentryDebugIds = {
168+
'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
169+
'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
170+
};
171+
172+
GLOBAL_OBJ._debugIds = {
173+
'filename3.js\nfilename3.js': 'cccccccc-cccc-4ccc-cccc-cccccccccc',
174+
'filename4.js\nfilename4.js': 'dddddddd-dddd-4ddd-dddd-dddddddddd',
175+
};
176+
177+
const stackParser = createStackParser([0, line => ({ filename: line })]);
178+
179+
const event: Event = {
180+
exception: {
181+
values: [
182+
{
183+
stacktrace: {
184+
frames: [
185+
{ filename: 'filename1.js' },
186+
{ filename: 'filename2.js' },
187+
{ filename: 'filename3.js' },
188+
{ filename: 'filename4.js' },
189+
],
190+
},
191+
},
192+
],
193+
},
194+
};
195+
196+
applyDebugIds(event, stackParser);
197+
198+
// Should have debug IDs from both sources
199+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
200+
filename: 'filename1.js',
201+
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
202+
});
203+
204+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
205+
filename: 'filename2.js',
206+
debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
207+
});
208+
209+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
210+
filename: 'filename3.js',
211+
debug_id: 'cccccccc-cccc-4ccc-cccc-cccccccccc',
212+
});
213+
214+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
215+
filename: 'filename4.js',
216+
debug_id: 'dddddddd-dddd-4ddd-dddd-dddddddddd',
217+
});
218+
});
219+
220+
it('should prioritize _debugIds over _sentryDebugIds for the same file', () => {
221+
GLOBAL_OBJ._sentryDebugIds = {
222+
'filename1.js\nfilename1.js': 'old-debug-id-aaaa-aaaa-aaaa-aaaaaaaaaa',
223+
};
224+
225+
GLOBAL_OBJ._debugIds = {
226+
'filename1.js\nfilename1.js': 'new-debug-id-bbbb-bbbb-bbbb-bbbbbbbbbb',
227+
};
228+
229+
const stackParser = createStackParser([0, line => ({ filename: line })]);
230+
231+
const event: Event = {
232+
exception: {
233+
values: [
234+
{
235+
stacktrace: {
236+
frames: [{ filename: 'filename1.js' }],
237+
},
238+
},
239+
],
240+
},
241+
};
242+
243+
applyDebugIds(event, stackParser);
244+
245+
// Should use the newer native _debugIds format
246+
expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({
247+
filename: 'filename1.js',
248+
debug_id: 'new-debug-id-bbbb-bbbb-bbbb-bbbbbbbbbb',
249+
});
250+
});
117251
});
118252

119253
describe('applyDebugMeta', () => {

packages/nextjs/src/config/handleRunAfterProductionCompile.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import type { SentryBuildOptions } from './types';
88
* It is used to upload sourcemaps to Sentry.
99
*/
1010
export async function handleRunAfterProductionCompile(
11-
{ releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' },
11+
{
12+
releaseName,
13+
distDir,
14+
buildTool,
15+
usesNativeDebugIds,
16+
}: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack'; usesNativeDebugIds?: boolean },
1217
sentryBuildOptions: SentryBuildOptions,
1318
): Promise<void> {
1419
if (sentryBuildOptions.debug) {
@@ -44,7 +49,11 @@ export async function handleRunAfterProductionCompile(
4449

4550
await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
4651
await sentryBuildPluginManager.createRelease();
47-
await sentryBuildPluginManager.injectDebugIds([distDir]);
52+
53+
if (!usesNativeDebugIds) {
54+
await sentryBuildPluginManager.injectDebugIds([distDir]);
55+
}
56+
4857
await sentryBuildPluginManager.uploadSourcemaps([distDir], {
4958
// We don't want to prepare the artifacts because we injected debug ids manually before
5059
prepareArtifacts: false,

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,37 @@
11
import { debug } from '@sentry/core';
22
import type { RouteManifest } from '../manifest/types';
3-
import type { NextConfigObject, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
3+
import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types';
4+
import { supportsNativeDebugIds } from '../util';
45
import { generateValueInjectionRules } from './generateValueInjectionRules';
56

67
/**
78
* Construct a Turbopack config object from a Next.js config object and a Turbopack options object.
89
*
910
* @param userNextConfig - The Next.js config object.
10-
* @param turbopackOptions - The Turbopack options object.
11+
* @param userSentryOptions - The Sentry build options object.
12+
* @param routeManifest - The route manifest object.
13+
* @param nextJsVersion - The Next.js version.
1114
* @returns The Turbopack config object.
1215
*/
1316
export function constructTurbopackConfig({
1417
userNextConfig,
18+
userSentryOptions,
1519
routeManifest,
1620
nextJsVersion,
1721
}: {
1822
userNextConfig: NextConfigObject;
23+
userSentryOptions: SentryBuildOptions;
1924
routeManifest?: RouteManifest;
2025
nextJsVersion?: string;
2126
}): TurbopackOptions {
27+
// If sourcemaps are disabled, we don't need to enable native debug ids as this will add build time.
28+
const shouldEnableNativeDebugIds =
29+
(supportsNativeDebugIds(nextJsVersion ?? '') && userNextConfig?.turbopack?.debugIds) ??
30+
userSentryOptions.sourcemaps?.disable !== true;
31+
2232
const newConfig: TurbopackOptions = {
2333
...userNextConfig.turbopack,
34+
...(shouldEnableNativeDebugIds ? { debugIds: true } : {}),
2435
};
2536

2637
const valueInjectionRules = generateValueInjectionRules({

packages/nextjs/src/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,4 +673,5 @@ export interface TurbopackOptions {
673673
conditions?: Record<string, TurbopackRuleCondition>;
674674
moduleIds?: 'named' | 'deterministic';
675675
root?: string;
676+
debugIds?: boolean;
676677
}

0 commit comments

Comments
 (0)