From 3d4a81937bcf00a2d3eaa066a4efea0dce520b72 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 2 Oct 2025 17:58:40 +0200 Subject: [PATCH 1/3] enable option --- .../config/turbopack/constructTurbopackConfig.ts | 15 +++++++++++++-- packages/nextjs/src/config/types.ts | 1 + packages/nextjs/src/config/util.ts | 15 +++++++++++++++ packages/nextjs/src/config/withSentryConfig.ts | 1 + 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts index 5c6372d6dec1..e46d3f6bb5c7 100644 --- a/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts +++ b/packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts @@ -1,26 +1,37 @@ import { debug } from '@sentry/core'; import type { RouteManifest } from '../manifest/types'; -import type { NextConfigObject, TurbopackMatcherWithRule, TurbopackOptions } from '../types'; +import type { NextConfigObject, SentryBuildOptions, TurbopackMatcherWithRule, TurbopackOptions } from '../types'; +import { supportsNativeDebugIds } from '../util'; import { generateValueInjectionRules } from './generateValueInjectionRules'; /** * Construct a Turbopack config object from a Next.js config object and a Turbopack options object. * * @param userNextConfig - The Next.js config object. - * @param turbopackOptions - The Turbopack options object. + * @param userSentryOptions - The Sentry build options object. + * @param routeManifest - The route manifest object. + * @param nextJsVersion - The Next.js version. * @returns The Turbopack config object. */ export function constructTurbopackConfig({ userNextConfig, + userSentryOptions, routeManifest, nextJsVersion, }: { userNextConfig: NextConfigObject; + userSentryOptions: SentryBuildOptions; routeManifest?: RouteManifest; nextJsVersion?: string; }): TurbopackOptions { + // If sourcemaps are disabled, we don't need to enable native debug ids as this will add build time. + const shouldEnableNativeDebugIds = + (supportsNativeDebugIds(nextJsVersion ?? '') && userNextConfig?.turbopack?.debugIds) ?? + userSentryOptions.sourcemaps?.disable !== true; + const newConfig: TurbopackOptions = { ...userNextConfig.turbopack, + ...(shouldEnableNativeDebugIds ? { debugIds: true } : {}), }; const valueInjectionRules = generateValueInjectionRules({ diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index 1fa245412f2c..28e038b6d0f2 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -673,4 +673,5 @@ export interface TurbopackOptions { conditions?: Record; moduleIds?: 'named' | 'deterministic'; root?: string; + debugIds?: boolean; } diff --git a/packages/nextjs/src/config/util.ts b/packages/nextjs/src/config/util.ts index de8ad68cac41..840b7377e574 100644 --- a/packages/nextjs/src/config/util.ts +++ b/packages/nextjs/src/config/util.ts @@ -65,3 +65,18 @@ export function supportsProductionCompileHook(version: string): boolean { return false; } + +/** + * Checks if the current Next.js version supports native debug ids for turbopack. + * This feature was first introduced in Next.js v15.6.0-canary.36 + * + * @param version - version string to check. + * @returns true if Next.js version supports native debug ids for turbopack builds + */ +export function supportsNativeDebugIds(version: string): boolean { + // tbd + if (version === '15.6.0-canary.36') { + return true; + } + return false; +} diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index ddf761998e50..3da0959ae20c 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -384,6 +384,7 @@ function getFinalConfigObject( userNextConfig: incomingUserNextConfigObject, routeManifest, nextJsVersion, + userSentryOptions, }), } : {}), From cf156c35a1a2e0e1438f155ebc683af9d65c28cd Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 3 Oct 2025 10:59:43 +0200 Subject: [PATCH 2/3] skip debug id injection for turbopack --- .../config/handleRunAfterProductionCompile.ts | 13 +++++-- .../nextjs/src/config/withSentryConfig.ts | 34 ++++++++++++++----- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts index c8dc35918198..d5c90962e581 100644 --- a/packages/nextjs/src/config/handleRunAfterProductionCompile.ts +++ b/packages/nextjs/src/config/handleRunAfterProductionCompile.ts @@ -8,7 +8,12 @@ import type { SentryBuildOptions } from './types'; * It is used to upload sourcemaps to Sentry. */ export async function handleRunAfterProductionCompile( - { releaseName, distDir, buildTool }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack' }, + { + releaseName, + distDir, + buildTool, + usesNativeDebugIds, + }: { releaseName?: string; distDir: string; buildTool: 'webpack' | 'turbopack'; usesNativeDebugIds?: boolean }, sentryBuildOptions: SentryBuildOptions, ): Promise { if (sentryBuildOptions.debug) { @@ -44,7 +49,11 @@ export async function handleRunAfterProductionCompile( await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal(); await sentryBuildPluginManager.createRelease(); - await sentryBuildPluginManager.injectDebugIds([distDir]); + + if (!usesNativeDebugIds) { + await sentryBuildPluginManager.injectDebugIds([distDir]); + } + await sentryBuildPluginManager.uploadSourcemaps([distDir], { // We don't want to prepare the artifacts because we injected debug ids manually before prepareArtifacts: false, diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 3da0959ae20c..f0cec4eed67a 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -14,6 +14,7 @@ import type { NextConfigFunction, NextConfigObject, SentryBuildOptions, + TurbopackOptions, } from './types'; import { getNextjsVersion, supportsProductionCompileHook } from './util'; import { constructWebpackConfigFunction } from './webpack'; @@ -287,6 +288,17 @@ function getFinalConfigObject( ); } + let turboPackConfig: TurbopackOptions | undefined; + + if (isTurbopack) { + turboPackConfig = constructTurbopackConfig({ + userNextConfig: incomingUserNextConfigObject, + userSentryOptions, + routeManifest, + nextJsVersion, + }); + } + // If not explicitly set, turbopack uses the runAfterProductionCompile hook (as there are no alternatives), webpack does not. const shouldUseRunAfterProductionCompileHook = userSentryOptions?.useRunAfterProductionCompileHook ?? (isTurbopack ? true : false); @@ -294,9 +306,15 @@ function getFinalConfigObject( if (shouldUseRunAfterProductionCompileHook && supportsProductionCompileHook(nextJsVersion ?? '')) { if (incomingUserNextConfigObject?.compiler?.runAfterProductionCompile === undefined) { incomingUserNextConfigObject.compiler ??= {}; + incomingUserNextConfigObject.compiler.runAfterProductionCompile = async ({ distDir }) => { await handleRunAfterProductionCompile( - { releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' }, + { + releaseName, + distDir, + buildTool: isTurbopack ? 'turbopack' : 'webpack', + usesNativeDebugIds: isTurbopack ? turboPackConfig?.debugIds : undefined, + }, userSentryOptions, ); }; @@ -308,7 +326,12 @@ function getFinalConfigObject( const { distDir }: { distDir: string } = argArray[0] ?? { distDir: '.next' }; await target.apply(thisArg, argArray); await handleRunAfterProductionCompile( - { releaseName, distDir, buildTool: isTurbopack ? 'turbopack' : 'webpack' }, + { + releaseName, + distDir, + buildTool: isTurbopack ? 'turbopack' : 'webpack', + usesNativeDebugIds: isTurbopack ? turboPackConfig?.debugIds : undefined, + }, userSentryOptions, ); }, @@ -380,12 +403,7 @@ function getFinalConfigObject( }), ...(isTurbopackSupported && isTurbopack ? { - turbopack: constructTurbopackConfig({ - userNextConfig: incomingUserNextConfigObject, - routeManifest, - nextJsVersion, - userSentryOptions, - }), + turbopack: turboPackConfig, } : {}), }; From 55953489345245f77d6572d40c93aac7afe53d39 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 3 Oct 2025 13:32:40 +0200 Subject: [PATCH 3/3] inject debugIds into globalObj --- packages/core/src/utils/debug-ids.ts | 82 ++++++++---- packages/core/src/utils/worldwide.ts | 6 + packages/core/test/lib/prepareEvent.test.ts | 134 ++++++++++++++++++++ 3 files changed, 194 insertions(+), 28 deletions(-) diff --git a/packages/core/src/utils/debug-ids.ts b/packages/core/src/utils/debug-ids.ts index f60e74c7cd26..97f30bbe816a 100644 --- a/packages/core/src/utils/debug-ids.ts +++ b/packages/core/src/utils/debug-ids.ts @@ -6,56 +6,82 @@ type StackString = string; type CachedResult = [string, string]; let parsedStackResults: Record | undefined; -let lastKeysCount: number | undefined; +let lastSentryKeysCount: number | undefined; +let lastNativeKeysCount: number | undefined; let cachedFilenameDebugIds: Record | undefined; /** * Returns a map of filenames to debug identifiers. + * Supports both proprietary _sentryDebugIds and native _debugIds (e.g., from Vercel) formats. */ export function getFilenameToDebugIdMap(stackParser: StackParser): Record { - const debugIdMap = GLOBAL_OBJ._sentryDebugIds; - if (!debugIdMap) { + const sentryDebugIdMap = GLOBAL_OBJ._sentryDebugIds; + const nativeDebugIdMap = GLOBAL_OBJ._debugIds; + + if (!sentryDebugIdMap && !nativeDebugIdMap) { return {}; } - const debugIdKeys = Object.keys(debugIdMap); + const sentryDebugIdKeys = sentryDebugIdMap ? Object.keys(sentryDebugIdMap) : []; + const nativeDebugIdKeys = nativeDebugIdMap ? Object.keys(nativeDebugIdMap) : []; // If the count of registered globals hasn't changed since the last call, we // can just return the cached result. - if (cachedFilenameDebugIds && debugIdKeys.length === lastKeysCount) { + if ( + cachedFilenameDebugIds && + sentryDebugIdKeys.length === lastSentryKeysCount && + nativeDebugIdKeys.length === lastNativeKeysCount + ) { return cachedFilenameDebugIds; } - lastKeysCount = debugIdKeys.length; - - // Build a map of filename -> debug_id. - cachedFilenameDebugIds = debugIdKeys.reduce>((acc, stackKey) => { - if (!parsedStackResults) { - parsedStackResults = {}; - } + lastSentryKeysCount = sentryDebugIdKeys.length; + lastNativeKeysCount = nativeDebugIdKeys.length; - const result = parsedStackResults[stackKey]; + // Build a map of filename -> debug_id from both sources + cachedFilenameDebugIds = {}; - if (result) { - acc[result[0]] = result[1]; - } else { - const parsedStack = stackParser(stackKey); - - for (let i = parsedStack.length - 1; i >= 0; i--) { - const stackFrame = parsedStack[i]; - const filename = stackFrame?.filename; - const debugId = debugIdMap[stackKey]; + if (!parsedStackResults) { + parsedStackResults = {}; + } - if (filename && debugId) { - acc[filename] = debugId; - parsedStackResults[stackKey] = [filename, debugId]; - break; + const processDebugIds = (debugIdKeys: string[], debugIdMap: Record): void => { + for (const key of debugIdKeys) { + const debugId = debugIdMap[key]; + const result = parsedStackResults?.[key]; + + if (result && cachedFilenameDebugIds && debugId) { + // Use cached filename but update with current debug ID + cachedFilenameDebugIds[result[0]] = debugId; + // Update cached result with new debug ID + if (parsedStackResults) { + parsedStackResults[key] = [result[0], debugId]; + } + } else if (debugId) { + const parsedStack = stackParser(key); + + for (let i = parsedStack.length - 1; i >= 0; i--) { + const stackFrame = parsedStack[i]; + const filename = stackFrame?.filename; + + if (filename && cachedFilenameDebugIds && parsedStackResults) { + cachedFilenameDebugIds[filename] = debugId; + parsedStackResults[key] = [filename, debugId]; + break; + } } } } + }; + + if (sentryDebugIdMap) { + processDebugIds(sentryDebugIdKeys, sentryDebugIdMap); + } - return acc; - }, {}); + // Native _debugIds will override _sentryDebugIds if same file + if (nativeDebugIdMap) { + processDebugIds(nativeDebugIdKeys, nativeDebugIdMap); + } return cachedFilenameDebugIds; } diff --git a/packages/core/src/utils/worldwide.ts b/packages/core/src/utils/worldwide.ts index e2f1ad5fc2b2..1955014f1345 100644 --- a/packages/core/src/utils/worldwide.ts +++ b/packages/core/src/utils/worldwide.ts @@ -41,6 +41,12 @@ export type InternalGlobal = { * file. */ _sentryDebugIds?: Record; + /** + * Native debug IDs implementation (e.g., from Vercel). + * This uses the same format as _sentryDebugIds but with a different global name. + * Keys are `error.stack` strings, values are debug IDs. + */ + _debugIds?: Record; /** * Raw module metadata that is injected by bundler plugins. * diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index d0fd86ae63f8..6472d3680fb0 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -19,6 +19,7 @@ import { clearGlobalScope } from '../testutils'; describe('applyDebugIds', () => { afterEach(() => { GLOBAL_OBJ._sentryDebugIds = undefined; + GLOBAL_OBJ._debugIds = undefined; }); it("should put debug IDs into an event's stack frames", () => { @@ -114,6 +115,139 @@ describe('applyDebugIds', () => { debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', }); }); + + it('should support native _debugIds format', () => { + GLOBAL_OBJ._debugIds = { + 'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + 'filename4.js\nfilename4.js': 'cccccccc-cccc-4ccc-cccc-cccccccccc', + }; + + const stackParser = createStackParser([0, line => ({ filename: line })]); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { filename: 'filename1.js' }, + { filename: 'filename2.js' }, + { filename: 'filename1.js' }, + { filename: 'filename3.js' }, + ], + }, + }, + ], + }, + }; + + applyDebugIds(event, stackParser); + + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename1.js', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + }); + + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename2.js', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }); + + // expect not to contain an image for the stack frame that doesn't have a corresponding debug id + expect(event.exception?.values?.[0]?.stacktrace?.frames).not.toContainEqual( + expect.objectContaining({ + filename3: 'filename3.js', + debug_id: expect.any(String), + }), + ); + }); + + it('should merge both _sentryDebugIds and _debugIds when both exist', () => { + GLOBAL_OBJ._sentryDebugIds = { + 'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + 'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }; + + GLOBAL_OBJ._debugIds = { + 'filename3.js\nfilename3.js': 'cccccccc-cccc-4ccc-cccc-cccccccccc', + 'filename4.js\nfilename4.js': 'dddddddd-dddd-4ddd-dddd-dddddddddd', + }; + + const stackParser = createStackParser([0, line => ({ filename: line })]); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { filename: 'filename1.js' }, + { filename: 'filename2.js' }, + { filename: 'filename3.js' }, + { filename: 'filename4.js' }, + ], + }, + }, + ], + }, + }; + + applyDebugIds(event, stackParser); + + // Should have debug IDs from both sources + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename1.js', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + }); + + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename2.js', + debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', + }); + + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename3.js', + debug_id: 'cccccccc-cccc-4ccc-cccc-cccccccccc', + }); + + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename4.js', + debug_id: 'dddddddd-dddd-4ddd-dddd-dddddddddd', + }); + }); + + it('should prioritize _debugIds over _sentryDebugIds for the same file', () => { + GLOBAL_OBJ._sentryDebugIds = { + 'filename1.js\nfilename1.js': 'old-debug-id-aaaa-aaaa-aaaa-aaaaaaaaaa', + }; + + GLOBAL_OBJ._debugIds = { + 'filename1.js\nfilename1.js': 'new-debug-id-bbbb-bbbb-bbbb-bbbbbbbbbb', + }; + + const stackParser = createStackParser([0, line => ({ filename: line })]); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [{ filename: 'filename1.js' }], + }, + }, + ], + }, + }; + + applyDebugIds(event, stackParser); + + // Should use the newer native _debugIds format + expect(event.exception?.values?.[0]?.stacktrace?.frames).toContainEqual({ + filename: 'filename1.js', + debug_id: 'new-debug-id-bbbb-bbbb-bbbb-bbbbbbbbbb', + }); + }); }); describe('applyDebugMeta', () => {