Skip to content
Open
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
27 changes: 21 additions & 6 deletions packages/core/src/utils/debug-ids.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DebugImage } from '../types-hoist/debugMeta';
import type { StackParser } from '../types-hoist/stacktrace';
import { normalizeStackTracePath } from './stacktrace';
import { GLOBAL_OBJ } from './worldwide';

type StackString = string;
Expand All @@ -10,6 +11,17 @@ let lastSentryKeysCount: number | undefined;
let lastNativeKeysCount: number | undefined;
let cachedFilenameDebugIds: Record<string, string> | undefined;

/**
* Clears the cached debug ID mappings.
* Useful for testing or when the global debug ID state changes.
*/
export function clearDebugIdCache(): void {
parsedStackResults = undefined;
lastSentryKeysCount = undefined;
lastNativeKeysCount = undefined;
cachedFilenameDebugIds = undefined;
}

/**
* Returns a map of filenames to debug identifiers.
* Supports both proprietary _sentryDebugIds and native _debugIds (e.g., from Vercel) formats.
Expand Down Expand Up @@ -101,12 +113,15 @@ export function getDebugImagesForResources(

const images: DebugImage[] = [];
for (const path of resource_paths) {
if (path && filenameDebugIdMap[path]) {
images.push({
type: 'sourcemap',
code_file: path,
debug_id: filenameDebugIdMap[path],
});
const normalizedPath = normalizeStackTracePath(path);
if (normalizedPath) {
if (filenameDebugIdMap[normalizedPath]) {
images.push({
type: 'sourcemap',
code_file: path,
debug_id: filenameDebugIdMap[normalizedPath],
});
}
}
}

Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/utils/node-stack-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// THE SOFTWARE.

import type { StackLineParser, StackLineParserFn } from '../types-hoist/stacktrace';
import { UNKNOWN_FUNCTION } from './stacktrace';
import { normalizeStackTracePath, UNKNOWN_FUNCTION } from './stacktrace';

export type GetModuleFn = (filename: string | undefined) => string | undefined;

Expand Down Expand Up @@ -55,7 +55,6 @@ export function node(getModule?: GetModuleFn): StackLineParserFn {
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/;
const DATA_URI_MATCH = /at (?:async )?(.+?) \(data:(.*?),/;

// eslint-disable-next-line complexity
return (line: string) => {
const dataUriMatch = line.match(DATA_URI_MATCH);
if (dataUriMatch) {
Expand Down Expand Up @@ -109,14 +108,9 @@ export function node(getModule?: GetModuleFn): StackLineParserFn {
functionName = typeName ? `${typeName}.${methodName}` : methodName;
}

let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2];
let filename = normalizeStackTracePath(lineMatch[2]);
const isNative = lineMatch[5] === 'native';

// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
if (filename?.match(/\/[A-Z]:/)) {
filename = filename.slice(1);
}

if (!filename && lineMatch[5] && !isNative) {
filename = lineMatch[5];
}
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/utils/stacktrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,15 @@ export function getVueInternalName(value: VueViewModel | VNode): string {

return isVNode ? '[VueVNode]' : '[VueViewModel]';
}

/**
* Normalizes stack line paths by removing file:// prefix and leading slashes for Windows paths
*/
export function normalizeStackTracePath(path: string | undefined): string | undefined {
let filename = path?.startsWith('file://') ? path.slice(7) : path;
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
if (filename?.match(/\/[A-Z]:/)) {
filename = filename.slice(1);
}
return filename;
}
187 changes: 187 additions & 0 deletions packages/core/test/lib/utils/debug-ids.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { nodeStackLineParser } from '../../../src';
import { clearDebugIdCache, getDebugImagesForResources, getFilenameToDebugIdMap } from '../../../src/utils/debug-ids';
import { createStackParser } from '../../../src/utils/stacktrace';

const nodeStackParser = createStackParser(nodeStackLineParser());

describe('getDebugImagesForResources', () => {
beforeEach(() => {
// Clear any existing debug ID maps
delete (globalThis as any)._sentryDebugIds;
delete (globalThis as any)._debugIds;
clearDebugIdCache();
});

it('should return debug images for resources without file:// prefix', () => {
// Setup debug IDs
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
};

const resources = ['/var/task/index.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(1);
expect(images[0]).toEqual({
type: 'sourcemap',
code_file: '/var/task/index.js',
debug_id: 'debug-id-123',
});
});

it('should return debug images for resources with file:// prefix', () => {
// Setup debug IDs - the stack parser strips file:// when parsing
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
};

// V8 profiler returns resources WITH file:// prefix
const resources = ['file:///var/task/index.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(1);
expect(images[0]).toEqual({
type: 'sourcemap',
code_file: 'file:///var/task/index.js',
debug_id: 'debug-id-123',
});
});

it('should handle mixed resources with and without file:// prefix', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
'at anotherFunction (/var/task/utils.js:10:5)': 'debug-id-456',
};

const resources = ['file:///var/task/index.js', '/var/task/utils.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(2);
expect(images[0]).toEqual({
type: 'sourcemap',
code_file: 'file:///var/task/index.js',
debug_id: 'debug-id-123',
});
expect(images[1]).toEqual({
type: 'sourcemap',
code_file: '/var/task/utils.js',
debug_id: 'debug-id-456',
});
});

it('should return empty array when no debug IDs match', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
};

const resources = ['file:///var/task/other.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(0);
});

it('should return empty array when no debug IDs are registered', () => {
const resources = ['file:///var/task/index.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(0);
});

it('should handle empty resource paths array', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
};

const resources: string[] = [];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(0);
});

it('should handle Windows paths with file:// prefix', () => {
// Stack parser normalizes Windows paths: file:///C:/foo.js -> C:/foo.js
(globalThis as any)._sentryDebugIds = {
'at mockFunction (C:/Users/dev/project/index.js:1:1)': 'debug-id-win-123',
};

// V8 profiler returns Windows paths with file:// prefix
const resources = ['file:///C:/Users/dev/project/index.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(1);
expect(images[0]).toEqual({
type: 'sourcemap',
code_file: 'file:///C:/Users/dev/project/index.js',
debug_id: 'debug-id-win-123',
});
});

it('should handle Windows paths without file:// prefix', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (C:/Users/dev/project/index.js:1:1)': 'debug-id-win-123',
};

const resources = ['C:/Users/dev/project/index.js'];
const images = getDebugImagesForResources(nodeStackParser, resources);

expect(images).toHaveLength(1);
expect(images[0]).toEqual({
type: 'sourcemap',
code_file: 'C:/Users/dev/project/index.js',
debug_id: 'debug-id-win-123',
});
});
});

describe('getFilenameToDebugIdMap', () => {
beforeEach(() => {
delete (globalThis as any)._sentryDebugIds;
delete (globalThis as any)._debugIds;
clearDebugIdCache();
});

it('should return empty object when no debug IDs are registered', () => {
const map = getFilenameToDebugIdMap(nodeStackParser);
expect(map).toEqual({});
});

it('should build map from _sentryDebugIds', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
'at anotherFunction (/var/task/utils.js:10:5)': 'debug-id-456',
};

const map = getFilenameToDebugIdMap(nodeStackParser);

expect(map).toEqual({
'/var/task/index.js': 'debug-id-123',
'/var/task/utils.js': 'debug-id-456',
});
});

it('should build map from native _debugIds', () => {
(globalThis as any)._debugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id-123',
};

const map = getFilenameToDebugIdMap(nodeStackParser);

expect(map).toEqual({
'/var/task/index.js': 'native-debug-id-123',
});
});

it('should prioritize native _debugIds over _sentryDebugIds', () => {
(globalThis as any)._sentryDebugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'sentry-debug-id',
};
(globalThis as any)._debugIds = {
'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id',
};

const map = getFilenameToDebugIdMap(nodeStackParser);

expect(map['/var/task/index.js']).toBe('native-debug-id');
});
});
Loading