diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 73a71e138db2..cce907c36446 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -1,5 +1,5 @@ -import type { ClientOptions, Event, EventHint } from '@sentry/types'; -import { dateTimestampInSeconds, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils'; +import type { ClientOptions, Event, EventHint, StackParser } from '@sentry/types'; +import { dateTimestampInSeconds, GLOBAL_OBJ, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils'; import { Scope } from '../scope'; @@ -36,6 +36,7 @@ export function prepareEvent( applyClientOptions(prepared, options); applyIntegrationsMetadata(prepared, integrations); + applyDebugMetadata(prepared, options.stackParser); // If we have scope given to us, use it as the base for further modifications. // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. @@ -112,6 +113,59 @@ function applyClientOptions(event: Event, options: ClientOptions): void { } } +/** + * Applies debug metadata images to the event in order to apply source maps by looking up their debug ID. + */ +export function applyDebugMetadata(event: Event, stackParser: StackParser): void { + const debugIdMap = GLOBAL_OBJ._sentryDebugIds; + + if (!debugIdMap) { + return; + } + + // Build a map of abs_path -> debug_id + const absPathDebugIdMap = Object.keys(debugIdMap).reduce>((acc, debugIdStackTrace) => { + const parsedStack = stackParser(debugIdStackTrace); + for (const stackFrame of parsedStack) { + if (stackFrame.abs_path) { + acc[stackFrame.abs_path] = debugIdMap[debugIdStackTrace]; + break; + } + } + return acc; + }, {}); + + // Get a Set of abs_paths in the stack trace + const errorAbsPaths = new Set(); + try { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + event!.exception!.values!.forEach(exception => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + exception.stacktrace!.frames!.forEach(frame => { + if (frame.abs_path) { + errorAbsPaths.add(frame.abs_path); + } + }); + }); + } catch (e) { + // To save bundle size we're just try catching here instead of checking for the existence of all the different objects. + } + + // Fill debug_meta information + event.debug_meta = event.debug_meta || {}; + event.debug_meta.images = event.debug_meta.images || []; + const images = event.debug_meta.images; + errorAbsPaths.forEach(absPath => { + if (absPathDebugIdMap[absPath]) { + images.push({ + type: 'sourcemap', + code_file: absPath, + debug_id: absPathDebugIdMap[absPath], + }); + } + }); +} + /** * This function adds all used integrations to the SDK info in the event. * @param event The event that will be filled with all integrations. diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts new file mode 100644 index 000000000000..b4cac98355e0 --- /dev/null +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -0,0 +1,68 @@ +import type { Event } from '@sentry/types'; +import { createStackParser, GLOBAL_OBJ } from '@sentry/utils'; + +import { applyDebugMetadata } from '../../src/utils/prepareEvent'; + +describe('applyDebugMetadata', () => { + afterEach(() => { + GLOBAL_OBJ._sentryDebugIds = undefined; + }); + + it('should put debug source map images in debug_meta field', () => { + GLOBAL_OBJ._sentryDebugIds = { + '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, abs_path: line })]); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { abs_path: 'filename1.js', filename: 'filename1.js' }, + { abs_path: 'filename2.js', filename: 'filename2.js' }, + { abs_path: 'filename1.js', filename: 'filename1.js' }, + { abs_path: 'filename3.js', filename: 'filename3.js' }, + ], + }, + }, + ], + }, + }; + + applyDebugMetadata(event, stackParser); + + expect(event.debug_meta?.images).toContainEqual({ + type: 'sourcemap', + code_file: 'filename1.js', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + }); + + expect(event.debug_meta?.images).toContainEqual({ + type: 'sourcemap', + code_file: '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.debug_meta?.images).not.toContainEqual( + expect.objectContaining({ + type: 'sourcemap', + code_file: 'filename3.js', + }), + ); + + // expect not to contain an image for the debug id mapping that isn't contained in the stack trace + expect(event.debug_meta?.images).not.toContainEqual( + expect.objectContaining({ + type: 'sourcemap', + code_file: 'filename4.js', + debug_id: 'cccccccc-cccc-4ccc-cccc-cccccccccc', + }), + ); + }); +}); diff --git a/packages/types/src/debugMeta.ts b/packages/types/src/debugMeta.ts index d0cbfeb46118..2a6f201874da 100644 --- a/packages/types/src/debugMeta.ts +++ b/packages/types/src/debugMeta.ts @@ -5,18 +5,18 @@ export interface DebugMeta { images?: Array; } -/** - * Possible choices for debug images. - */ -export type DebugImageType = 'wasm' | 'macho' | 'elf' | 'pe'; +export type DebugImage = WasmDebugImage | SourceMapDebugImage; -/** - * References to debug images. - */ -export interface DebugImage { - type: DebugImageType; +interface WasmDebugImage { + type: 'wasm'; debug_id: string; code_id?: string | null; code_file: string; debug_file?: string | null; } + +interface SourceMapDebugImage { + type: 'sourcemap'; + code_file: string; // abs_path + debug_id: string; // uuid +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0b52fac74e02..06aaba7ff571 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -5,7 +5,7 @@ export type { ClientReport, Outcome, EventDropReason } from './clientreport'; export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext, TraceContext } from './context'; export type { DataCategory } from './datacategory'; export type { DsnComponents, DsnLike, DsnProtocol } from './dsn'; -export type { DebugImage, DebugImageType, DebugMeta } from './debugMeta'; +export type { DebugImage, DebugMeta } from './debugMeta'; export type { AttachmentItem, BaseEnvelopeHeaders, diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 8af1c6f0d1b0..ccf18c64e897 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,14 +1,7 @@ import type { StackFrame, StackLineParser, StackLineParserFn, StackParser } from '@sentry/types'; -import { GLOBAL_OBJ } from './worldwide'; - const STACKTRACE_LIMIT = 50; -type DebugIdFilename = string; -type DebugId = string; - -const debugIdParserCache = new Map>(); - /** * Creates a stack parser with the supplied line parsers * @@ -21,29 +14,6 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser { return (stack: string, skipFirst: number = 0): StackFrame[] => { const frames: StackFrame[] = []; - - for (const parser of sortedParsers) { - let debugIdCache = debugIdParserCache.get(parser); - if (!debugIdCache) { - debugIdCache = new Map(); - debugIdParserCache.set(parser, debugIdCache); - } - - const debugIdMap = GLOBAL_OBJ._sentryDebugIds; - - if (debugIdMap) { - Object.keys(debugIdMap).forEach(debugIdStackTrace => { - debugIdStackTrace.split('\n').forEach(line => { - const frame = parser(line); - if (frame && frame.filename) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - debugIdCache!.set(frame.filename, debugIdMap[debugIdStackTrace]); - } - }); - }); - } - } - for (const line of stack.split('\n').slice(skipFirst)) { // Ignore lines over 1kb as they are unlikely to be stack frames. // Many of the regular expressions use backtracking which results in run time that increases exponentially with @@ -61,14 +31,6 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser { const frame = parser(cleanedLine); if (frame) { - const debugIdCache = debugIdParserCache.get(parser); - if (debugIdCache && frame.filename) { - const cachedDebugId = debugIdCache.get(frame.filename); - if (cachedDebugId) { - frame.debug_id = cachedDebugId; - } - } - frames.push(frame); break; } diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts index 028eba2ac774..7b61814229cf 100644 --- a/packages/utils/src/worldwide.ts +++ b/packages/utils/src/worldwide.ts @@ -29,6 +29,11 @@ export interface InternalGlobal { id?: string; }; SENTRY_SDK_SOURCE?: SdkSource; + /** + * Debug IDs are indirectly injected by Sentry CLI or bundler plugins to directly reference a particular source map + * for resolving of a source file. The injected code will place an entry into the record for each loaded bundle/JS + * file. + */ _sentryDebugIds?: Record; __SENTRY__: { globalEventProcessors: any; diff --git a/packages/utils/test/stacktrace.test.ts b/packages/utils/test/stacktrace.test.ts index a2438c687bc4..61b44de34366 100644 --- a/packages/utils/test/stacktrace.test.ts +++ b/packages/utils/test/stacktrace.test.ts @@ -1,5 +1,4 @@ -import { createStackParser, stripSentryFramesAndReverse } from '../src/stacktrace'; -import { GLOBAL_OBJ } from '../src/worldwide'; +import { stripSentryFramesAndReverse } from '../src/stacktrace'; describe('Stacktrace', () => { describe('stripSentryFramesAndReverse()', () => { @@ -69,41 +68,3 @@ describe('Stacktrace', () => { }); }); }); - -describe('Stack parsers created with createStackParser', () => { - afterEach(() => { - GLOBAL_OBJ._sentryDebugIds = undefined; - }); - - it('put debug ids onto individual frames', () => { - GLOBAL_OBJ._sentryDebugIds = { - 'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - 'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - }; - - const fakeErrorStack = 'filename1.js\nfilename2.js\nfilename1.js\nfilename3.js'; - const stackParser = createStackParser([0, line => ({ filename: line })]); - - const result = stackParser(fakeErrorStack); - - expect(result[0]).toStrictEqual({ filename: 'filename3.js', function: '?' }); - - expect(result[1]).toStrictEqual({ - filename: 'filename1.js', - function: '?', - debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - }); - - expect(result[2]).toStrictEqual({ - filename: 'filename2.js', - function: '?', - debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb', - }); - - expect(result[3]).toStrictEqual({ - filename: 'filename1.js', - function: '?', - debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', - }); - }); -}); diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index 436668e73ac2..8d79586eb1d3 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -61,7 +61,7 @@ export class Wasm implements Integration { if (haveWasm) { event.debug_meta = event.debug_meta || {}; - event.debug_meta.images = getImages(); + event.debug_meta.images = [...(event.debug_meta.images || []), ...getImages()]; } return event; diff --git a/packages/wasm/src/registry.ts b/packages/wasm/src/registry.ts index f0b1069ed7e9..765b47026f00 100644 --- a/packages/wasm/src/registry.ts +++ b/packages/wasm/src/registry.ts @@ -41,7 +41,7 @@ export function getModuleInfo(module: WebAssembly.Module): ModuleInfo { export function registerModule(module: WebAssembly.Module, url: string): void { const { buildId, debugFile } = getModuleInfo(module); if (buildId) { - const oldIdx = IMAGES.findIndex(img => img.code_file === url); + const oldIdx = getImage(url); if (oldIdx >= 0) { IMAGES.splice(oldIdx, 1); } @@ -68,5 +68,7 @@ export function getImages(): Array { * @param url the URL of the WebAssembly module. */ export function getImage(url: string): number { - return IMAGES.findIndex(img => img.code_file === url); + return IMAGES.findIndex(image => { + return image.type === 'wasm' && image.code_file === url; + }); }