Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add source map images to debug_meta #7168

Merged
merged 16 commits into from
Feb 23, 2023
56 changes: 54 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,57 @@ 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
Member 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: Record<string, string> = {};
Object.keys(debugIdMap).forEach(debugIdStackTrace => {
Copy link
Member

Choose a reason for hiding this comment

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

l: Can this just be a single reduce call?

const parsedStack = stackParser(debugIdStackTrace);
for (const stackFrame of parsedStack) {
if (stackFrame.abs_path) {
absPathDebugIdMap[stackFrame.abs_path] = debugIdMap[debugIdStackTrace];
break;
}
}
});

// Get a Set of abs_paths in the stack trace
const errorAbsPaths = new Set<string>();
if (event && event.exception && event.exception.values) {
event.exception.values.forEach(exception => {
if (exception.stacktrace && exception.stacktrace.frames) {
exception.stacktrace.frames.forEach(frame => {
if (frame.abs_path) {
errorAbsPaths.add(frame.abs_path);
}
});
}
});
}
Copy link
Member

Choose a reason for hiding this comment

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

l: if we try catch this we can get rid of all the undefined checks ^^


// 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;
});
}