Skip to content

Commit ba390df

Browse files
authored
fix(node): Profiling debug ID matching
2 parents 65afa6a + f66969d commit ba390df

File tree

4 files changed

+216
-10
lines changed

4 files changed

+216
-10
lines changed

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { DebugImage } from '../types-hoist/debugMeta';
22
import type { StackParser } from '../types-hoist/stacktrace';
3+
import { normalizeStackTracePath } from './stacktrace';
34
import { GLOBAL_OBJ } from './worldwide';
45

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

14+
/**
15+
* Clears the cached debug ID mappings.
16+
* Useful for testing or when the global debug ID state changes.
17+
*/
18+
export function clearDebugIdCache(): void {
19+
parsedStackResults = undefined;
20+
lastSentryKeysCount = undefined;
21+
lastNativeKeysCount = undefined;
22+
cachedFilenameDebugIds = undefined;
23+
}
24+
1325
/**
1426
* Returns a map of filenames to debug identifiers.
1527
* Supports both proprietary _sentryDebugIds and native _debugIds (e.g., from Vercel) formats.
@@ -101,11 +113,12 @@ export function getDebugImagesForResources(
101113

102114
const images: DebugImage[] = [];
103115
for (const path of resource_paths) {
104-
if (path && filenameDebugIdMap[path]) {
116+
const normalizedPath = normalizeStackTracePath(path);
117+
if (normalizedPath && filenameDebugIdMap[normalizedPath]) {
105118
images.push({
106119
type: 'sourcemap',
107120
code_file: path,
108-
debug_id: filenameDebugIdMap[path],
121+
debug_id: filenameDebugIdMap[normalizedPath],
109122
});
110123
}
111124
}

packages/core/src/utils/node-stack-trace.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
// THE SOFTWARE.
2323

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

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

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

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

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

115-
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
116-
if (filename?.match(/\/[A-Z]:/)) {
117-
filename = filename.slice(1);
118-
}
119-
120114
if (!filename && lineMatch[5] && !isNative) {
121115
filename = lineMatch[5];
122116
}

packages/core/src/utils/stacktrace.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,15 @@ export function getVueInternalName(value: VueViewModel | VNode): string {
177177

178178
return isVNode ? '[VueVNode]' : '[VueViewModel]';
179179
}
180+
181+
/**
182+
* Normalizes stack line paths by removing file:// prefix and leading slashes for Windows paths
183+
*/
184+
export function normalizeStackTracePath(path: string | undefined): string | undefined {
185+
let filename = path?.startsWith('file://') ? path.slice(7) : path;
186+
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
187+
if (filename?.match(/\/[A-Z]:/)) {
188+
filename = filename.slice(1);
189+
}
190+
return filename;
191+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import { nodeStackLineParser } from '../../../src';
3+
import { clearDebugIdCache, getDebugImagesForResources, getFilenameToDebugIdMap } from '../../../src/utils/debug-ids';
4+
import { createStackParser } from '../../../src/utils/stacktrace';
5+
6+
const nodeStackParser = createStackParser(nodeStackLineParser());
7+
8+
describe('getDebugImagesForResources', () => {
9+
beforeEach(() => {
10+
// Clear any existing debug ID maps
11+
delete (globalThis as any)._sentryDebugIds;
12+
delete (globalThis as any)._debugIds;
13+
clearDebugIdCache();
14+
});
15+
16+
it('should return debug images for resources without file:// prefix', () => {
17+
// Setup debug IDs
18+
(globalThis as any)._sentryDebugIds = {
19+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
20+
};
21+
22+
const resources = ['/var/task/index.js'];
23+
const images = getDebugImagesForResources(nodeStackParser, resources);
24+
25+
expect(images).toHaveLength(1);
26+
expect(images[0]).toEqual({
27+
type: 'sourcemap',
28+
code_file: '/var/task/index.js',
29+
debug_id: 'debug-id-123',
30+
});
31+
});
32+
33+
it('should return debug images for resources with file:// prefix', () => {
34+
// Setup debug IDs - the stack parser strips file:// when parsing
35+
(globalThis as any)._sentryDebugIds = {
36+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
37+
};
38+
39+
// V8 profiler returns resources WITH file:// prefix
40+
const resources = ['file:///var/task/index.js'];
41+
const images = getDebugImagesForResources(nodeStackParser, resources);
42+
43+
expect(images).toHaveLength(1);
44+
expect(images[0]).toEqual({
45+
type: 'sourcemap',
46+
code_file: 'file:///var/task/index.js',
47+
debug_id: 'debug-id-123',
48+
});
49+
});
50+
51+
it('should handle mixed resources with and without file:// prefix', () => {
52+
(globalThis as any)._sentryDebugIds = {
53+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
54+
'at anotherFunction (/var/task/utils.js:10:5)': 'debug-id-456',
55+
};
56+
57+
const resources = ['file:///var/task/index.js', '/var/task/utils.js'];
58+
const images = getDebugImagesForResources(nodeStackParser, resources);
59+
60+
expect(images).toHaveLength(2);
61+
expect(images[0]).toEqual({
62+
type: 'sourcemap',
63+
code_file: 'file:///var/task/index.js',
64+
debug_id: 'debug-id-123',
65+
});
66+
expect(images[1]).toEqual({
67+
type: 'sourcemap',
68+
code_file: '/var/task/utils.js',
69+
debug_id: 'debug-id-456',
70+
});
71+
});
72+
73+
it('should return empty array when no debug IDs match', () => {
74+
(globalThis as any)._sentryDebugIds = {
75+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
76+
};
77+
78+
const resources = ['file:///var/task/other.js'];
79+
const images = getDebugImagesForResources(nodeStackParser, resources);
80+
81+
expect(images).toHaveLength(0);
82+
});
83+
84+
it('should return empty array when no debug IDs are registered', () => {
85+
const resources = ['file:///var/task/index.js'];
86+
const images = getDebugImagesForResources(nodeStackParser, resources);
87+
88+
expect(images).toHaveLength(0);
89+
});
90+
91+
it('should handle empty resource paths array', () => {
92+
(globalThis as any)._sentryDebugIds = {
93+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
94+
};
95+
96+
const resources: string[] = [];
97+
const images = getDebugImagesForResources(nodeStackParser, resources);
98+
99+
expect(images).toHaveLength(0);
100+
});
101+
102+
it('should handle Windows paths with file:// prefix', () => {
103+
// Stack parser normalizes Windows paths: file:///C:/foo.js -> C:/foo.js
104+
(globalThis as any)._sentryDebugIds = {
105+
'at mockFunction (C:/Users/dev/project/index.js:1:1)': 'debug-id-win-123',
106+
};
107+
108+
// V8 profiler returns Windows paths with file:// prefix
109+
const resources = ['file:///C:/Users/dev/project/index.js'];
110+
const images = getDebugImagesForResources(nodeStackParser, resources);
111+
112+
expect(images).toHaveLength(1);
113+
expect(images[0]).toEqual({
114+
type: 'sourcemap',
115+
code_file: 'file:///C:/Users/dev/project/index.js',
116+
debug_id: 'debug-id-win-123',
117+
});
118+
});
119+
120+
it('should handle Windows paths without file:// prefix', () => {
121+
(globalThis as any)._sentryDebugIds = {
122+
'at mockFunction (C:/Users/dev/project/index.js:1:1)': 'debug-id-win-123',
123+
};
124+
125+
const resources = ['C:/Users/dev/project/index.js'];
126+
const images = getDebugImagesForResources(nodeStackParser, resources);
127+
128+
expect(images).toHaveLength(1);
129+
expect(images[0]).toEqual({
130+
type: 'sourcemap',
131+
code_file: 'C:/Users/dev/project/index.js',
132+
debug_id: 'debug-id-win-123',
133+
});
134+
});
135+
});
136+
137+
describe('getFilenameToDebugIdMap', () => {
138+
beforeEach(() => {
139+
delete (globalThis as any)._sentryDebugIds;
140+
delete (globalThis as any)._debugIds;
141+
clearDebugIdCache();
142+
});
143+
144+
it('should return empty object when no debug IDs are registered', () => {
145+
const map = getFilenameToDebugIdMap(nodeStackParser);
146+
expect(map).toEqual({});
147+
});
148+
149+
it('should build map from _sentryDebugIds', () => {
150+
(globalThis as any)._sentryDebugIds = {
151+
'at mockFunction (/var/task/index.js:1:1)': 'debug-id-123',
152+
'at anotherFunction (/var/task/utils.js:10:5)': 'debug-id-456',
153+
};
154+
155+
const map = getFilenameToDebugIdMap(nodeStackParser);
156+
157+
expect(map).toEqual({
158+
'/var/task/index.js': 'debug-id-123',
159+
'/var/task/utils.js': 'debug-id-456',
160+
});
161+
});
162+
163+
it('should build map from native _debugIds', () => {
164+
(globalThis as any)._debugIds = {
165+
'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id-123',
166+
};
167+
168+
const map = getFilenameToDebugIdMap(nodeStackParser);
169+
170+
expect(map).toEqual({
171+
'/var/task/index.js': 'native-debug-id-123',
172+
});
173+
});
174+
175+
it('should prioritize native _debugIds over _sentryDebugIds', () => {
176+
(globalThis as any)._sentryDebugIds = {
177+
'at mockFunction (/var/task/index.js:1:1)': 'sentry-debug-id',
178+
};
179+
(globalThis as any)._debugIds = {
180+
'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id',
181+
};
182+
183+
const map = getFilenameToDebugIdMap(nodeStackParser);
184+
185+
expect(map['/var/task/index.js']).toBe('native-debug-id');
186+
});
187+
});

0 commit comments

Comments
 (0)