Skip to content
Merged
58 changes: 56 additions & 2 deletions packages/core/src/utils/prepareEvent.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

Where is this _sentryDebugIds actually set? Can't find it anywhere in the codebase...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh I should definitely add a comment explaining this --> 485b981


if (!debugIdMap) {
return;
}

// Build a map of abs_path -> debug_id
const absPathDebugIdMap = Object.keys(debugIdMap).reduce<Record<string, string>>((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<string>();
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.
Expand Down
68 changes: 68 additions & 0 deletions packages/core/test/lib/prepareEvent.test.ts
Original file line number Diff line number Diff line change
@@ -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',
}),
);
});
});
18 changes: 9 additions & 9 deletions packages/types/src/debugMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ export interface DebugMeta {
images?: Array<DebugImage>;
}

/**
* 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
}
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
38 changes: 0 additions & 38 deletions packages/utils/src/stacktrace.ts
Original file line number Diff line number Diff line change
@@ -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<StackLineParserFn, Map<DebugIdFilename, DebugId>>();

/**
* Creates a stack parser with the supplied line parsers
*
Expand All @@ -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
Expand All @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/src/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
__SENTRY__: {
globalEventProcessors: any;
Expand Down
41 changes: 1 addition & 40 deletions packages/utils/test/stacktrace.test.ts
Original file line number Diff line number Diff line change
@@ -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()', () => {
Expand Down Expand Up @@ -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',
});
});
});
2 changes: 1 addition & 1 deletion packages/wasm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 4 additions & 2 deletions packages/wasm/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -68,5 +68,7 @@ export function getImages(): Array<DebugImage> {
* @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;
});
}