From ecb41114ca1e3364c7530977f554a84144ab1587 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 14 Jan 2026 15:18:18 +0100 Subject: [PATCH 1/2] fix(node): Profiling debug ID matching --- packages/core/src/utils/debug-ids.ts | 15 +- .../core/test/lib/utils/debug-ids.test.ts | 165 ++++++++++++++++++ 2 files changed, 174 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/lib/utils/debug-ids.test.ts diff --git a/packages/core/src/utils/debug-ids.ts b/packages/core/src/utils/debug-ids.ts index fd31009ae32d..7b11e4ebc280 100644 --- a/packages/core/src/utils/debug-ids.ts +++ b/packages/core/src/utils/debug-ids.ts @@ -101,12 +101,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], - }); + if (path) { + const normalizedPath = path.startsWith('file://') ? path.slice(7) : path; + if (filenameDebugIdMap[normalizedPath]) { + images.push({ + type: 'sourcemap', + code_file: path, + debug_id: filenameDebugIdMap[normalizedPath], + }); + } } } diff --git a/packages/core/test/lib/utils/debug-ids.test.ts b/packages/core/test/lib/utils/debug-ids.test.ts new file mode 100644 index 000000000000..6235c37cf628 --- /dev/null +++ b/packages/core/test/lib/utils/debug-ids.test.ts @@ -0,0 +1,165 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import type { StackParser } from '../../../src/types-hoist/stacktrace'; +import { getDebugImagesForResources, getFilenameToDebugIdMap } from '../../../src/utils/debug-ids'; + +describe('getDebugImagesForResources', () => { + const mockStackParser: StackParser = (stack: string) => { + // Simple mock that extracts filename from a stack string + const match = stack.match(/at .* \((.*?):\d+:\d+\)/); + if (match) { + return [{ filename: match[1], function: 'mockFunction', lineno: 1, colno: 1 }]; + } + return []; + }; + + beforeEach(() => { + // Clear any existing debug ID maps + delete (globalThis as any)._sentryDebugIds; + delete (globalThis as any)._debugIds; + }); + + 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(mockStackParser, 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(mockStackParser, 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(mockStackParser, 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(mockStackParser, 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(mockStackParser, 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(mockStackParser, resources); + + expect(images).toHaveLength(0); + }); +}); + +describe('getFilenameToDebugIdMap', () => { + const mockStackParser: StackParser = (stack: string) => { + const match = stack.match(/at .* \((.*?):\d+:\d+\)/); + if (match) { + return [{ filename: match[1], function: 'mockFunction', lineno: 1, colno: 1 }]; + } + return []; + }; + + beforeEach(() => { + delete (globalThis as any)._sentryDebugIds; + delete (globalThis as any)._debugIds; + }); + + it('should return empty object when no debug IDs are registered', () => { + const map = getFilenameToDebugIdMap(mockStackParser); + 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(mockStackParser); + + 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(mockStackParser); + + 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(mockStackParser); + + expect(map['/var/task/index.js']).toBe('native-debug-id'); + }); +}); From 5c08507335754b5f866ea795da2c5ae667868707 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 14 Jan 2026 16:01:41 +0100 Subject: [PATCH 2/2] Fix for windows and try and reduce bundle impact --- packages/core/src/utils/debug-ids.ts | 16 +++- packages/core/src/utils/node-stack-trace.ts | 10 +-- packages/core/src/utils/stacktrace.ts | 12 +++ .../core/test/lib/utils/debug-ids.test.ts | 80 ++++++++++++------- 4 files changed, 79 insertions(+), 39 deletions(-) diff --git a/packages/core/src/utils/debug-ids.ts b/packages/core/src/utils/debug-ids.ts index 7b11e4ebc280..32396b5bbcac 100644 --- a/packages/core/src/utils/debug-ids.ts +++ b/packages/core/src/utils/debug-ids.ts @@ -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; @@ -10,6 +11,17 @@ let lastSentryKeysCount: number | undefined; let lastNativeKeysCount: number | undefined; let cachedFilenameDebugIds: Record | 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. @@ -101,8 +113,8 @@ export function getDebugImagesForResources( const images: DebugImage[] = []; for (const path of resource_paths) { - if (path) { - const normalizedPath = path.startsWith('file://') ? path.slice(7) : path; + const normalizedPath = normalizeStackTracePath(path); + if (normalizedPath) { if (filenameDebugIdMap[normalizedPath]) { images.push({ type: 'sourcemap', diff --git a/packages/core/src/utils/node-stack-trace.ts b/packages/core/src/utils/node-stack-trace.ts index 0cecd3dbf1e9..1132471b0e8f 100644 --- a/packages/core/src/utils/node-stack-trace.ts +++ b/packages/core/src/utils/node-stack-trace.ts @@ -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; @@ -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) { @@ -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]; } diff --git a/packages/core/src/utils/stacktrace.ts b/packages/core/src/utils/stacktrace.ts index 6b50caf48b30..16a32ede4e58 100644 --- a/packages/core/src/utils/stacktrace.ts +++ b/packages/core/src/utils/stacktrace.ts @@ -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; +} diff --git a/packages/core/test/lib/utils/debug-ids.test.ts b/packages/core/test/lib/utils/debug-ids.test.ts index 6235c37cf628..45ff58c82565 100644 --- a/packages/core/test/lib/utils/debug-ids.test.ts +++ b/packages/core/test/lib/utils/debug-ids.test.ts @@ -1,21 +1,16 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import type { StackParser } from '../../../src/types-hoist/stacktrace'; -import { getDebugImagesForResources, getFilenameToDebugIdMap } from '../../../src/utils/debug-ids'; +import { nodeStackLineParser } from '../../../src'; +import { clearDebugIdCache, getDebugImagesForResources, getFilenameToDebugIdMap } from '../../../src/utils/debug-ids'; +import { createStackParser } from '../../../src/utils/stacktrace'; -describe('getDebugImagesForResources', () => { - const mockStackParser: StackParser = (stack: string) => { - // Simple mock that extracts filename from a stack string - const match = stack.match(/at .* \((.*?):\d+:\d+\)/); - if (match) { - return [{ filename: match[1], function: 'mockFunction', lineno: 1, colno: 1 }]; - } - return []; - }; +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', () => { @@ -25,7 +20,7 @@ describe('getDebugImagesForResources', () => { }; const resources = ['/var/task/index.js']; - const images = getDebugImagesForResources(mockStackParser, resources); + const images = getDebugImagesForResources(nodeStackParser, resources); expect(images).toHaveLength(1); expect(images[0]).toEqual({ @@ -43,7 +38,7 @@ describe('getDebugImagesForResources', () => { // V8 profiler returns resources WITH file:// prefix const resources = ['file:///var/task/index.js']; - const images = getDebugImagesForResources(mockStackParser, resources); + const images = getDebugImagesForResources(nodeStackParser, resources); expect(images).toHaveLength(1); expect(images[0]).toEqual({ @@ -60,7 +55,7 @@ describe('getDebugImagesForResources', () => { }; const resources = ['file:///var/task/index.js', '/var/task/utils.js']; - const images = getDebugImagesForResources(mockStackParser, resources); + const images = getDebugImagesForResources(nodeStackParser, resources); expect(images).toHaveLength(2); expect(images[0]).toEqual({ @@ -81,14 +76,14 @@ describe('getDebugImagesForResources', () => { }; const resources = ['file:///var/task/other.js']; - const images = getDebugImagesForResources(mockStackParser, resources); + 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(mockStackParser, resources); + const images = getDebugImagesForResources(nodeStackParser, resources); expect(images).toHaveLength(0); }); @@ -99,28 +94,55 @@ describe('getDebugImagesForResources', () => { }; const resources: string[] = []; - const images = getDebugImagesForResources(mockStackParser, resources); + 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', () => { - const mockStackParser: StackParser = (stack: string) => { - const match = stack.match(/at .* \((.*?):\d+:\d+\)/); - if (match) { - return [{ filename: match[1], function: 'mockFunction', lineno: 1, colno: 1 }]; - } - return []; - }; - 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(mockStackParser); + const map = getFilenameToDebugIdMap(nodeStackParser); expect(map).toEqual({}); }); @@ -130,7 +152,7 @@ describe('getFilenameToDebugIdMap', () => { 'at anotherFunction (/var/task/utils.js:10:5)': 'debug-id-456', }; - const map = getFilenameToDebugIdMap(mockStackParser); + const map = getFilenameToDebugIdMap(nodeStackParser); expect(map).toEqual({ '/var/task/index.js': 'debug-id-123', @@ -143,7 +165,7 @@ describe('getFilenameToDebugIdMap', () => { 'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id-123', }; - const map = getFilenameToDebugIdMap(mockStackParser); + const map = getFilenameToDebugIdMap(nodeStackParser); expect(map).toEqual({ '/var/task/index.js': 'native-debug-id-123', @@ -158,7 +180,7 @@ describe('getFilenameToDebugIdMap', () => { 'at mockFunction (/var/task/index.js:1:1)': 'native-debug-id', }; - const map = getFilenameToDebugIdMap(mockStackParser); + const map = getFilenameToDebugIdMap(nodeStackParser); expect(map['/var/task/index.js']).toBe('native-debug-id'); });