Skip to content
Draft
Show file tree
Hide file tree
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
82 changes: 54 additions & 28 deletions packages/core/src/utils/debug-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,82 @@ type StackString = string;
type CachedResult = [string, string];

let parsedStackResults: Record<StackString, CachedResult> | undefined;
let lastKeysCount: number | undefined;
let lastSentryKeysCount: number | undefined;
let lastNativeKeysCount: number | undefined;
let cachedFilenameDebugIds: Record<string, string> | 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<string, string> {
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<Record<string, string>>((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<string, string>): 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;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export type InternalGlobal = {
* file.
*/
_sentryDebugIds?: Record<string, string>;
/**
* 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<string, string>;
/**
* Raw module metadata that is injected by bundler plugins.
*
Expand Down
134 changes: 134 additions & 0 deletions packages/core/test/lib/prepareEvent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
13 changes: 11 additions & 2 deletions packages/nextjs/src/config/handleRunAfterProductionCompile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
if (sentryBuildOptions.debug) {
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions packages/nextjs/src/config/turbopack/constructTurbopackConfig.ts
Original file line number Diff line number Diff line change
@@ -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 } : {}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

q/l: is this something users could override (if for whatever reason they want to enable debugIds but not source maps upload in sentry)?

};

const valueInjectionRules = generateValueInjectionRules({
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,4 +673,5 @@ export interface TurbopackOptions {
conditions?: Record<string, TurbopackRuleCondition>;
moduleIds?: 'named' | 'deterministic';
root?: string;
debugIds?: boolean;
}
15 changes: 15 additions & 0 deletions packages/nextjs/src/config/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will still wait until we know in which stable version this lands

if (version === '15.6.0-canary.36') {
return true;
}
return false;
Comment on lines +78 to +81
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-l: we can simplify this.

Suggested change
if (version === '15.6.0-canary.36') {
return true;
}
return false;
return version === '15.6.0-canary.36'

}
Loading
Loading