Skip to content

Commit e4d3146

Browse files
authored
feat: Add source map images to debug_meta (#7168)
1 parent e24127c commit e4d3146

File tree

9 files changed

+145
-93
lines changed

9 files changed

+145
-93
lines changed

packages/core/src/utils/prepareEvent.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { ClientOptions, Event, EventHint } from '@sentry/types';
2-
import { dateTimestampInSeconds, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils';
1+
import type { ClientOptions, Event, EventHint, StackParser } from '@sentry/types';
2+
import { dateTimestampInSeconds, GLOBAL_OBJ, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils';
33

44
import { Scope } from '../scope';
55

@@ -36,6 +36,7 @@ export function prepareEvent(
3636

3737
applyClientOptions(prepared, options);
3838
applyIntegrationsMetadata(prepared, integrations);
39+
applyDebugMetadata(prepared, options.stackParser);
3940

4041
// If we have scope given to us, use it as the base for further modifications.
4142
// 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 {
112113
}
113114
}
114115

116+
/**
117+
* Applies debug metadata images to the event in order to apply source maps by looking up their debug ID.
118+
*/
119+
export function applyDebugMetadata(event: Event, stackParser: StackParser): void {
120+
const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
121+
122+
if (!debugIdMap) {
123+
return;
124+
}
125+
126+
// Build a map of abs_path -> debug_id
127+
const absPathDebugIdMap = Object.keys(debugIdMap).reduce<Record<string, string>>((acc, debugIdStackTrace) => {
128+
const parsedStack = stackParser(debugIdStackTrace);
129+
for (const stackFrame of parsedStack) {
130+
if (stackFrame.abs_path) {
131+
acc[stackFrame.abs_path] = debugIdMap[debugIdStackTrace];
132+
break;
133+
}
134+
}
135+
return acc;
136+
}, {});
137+
138+
// Get a Set of abs_paths in the stack trace
139+
const errorAbsPaths = new Set<string>();
140+
try {
141+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
142+
event!.exception!.values!.forEach(exception => {
143+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
144+
exception.stacktrace!.frames!.forEach(frame => {
145+
if (frame.abs_path) {
146+
errorAbsPaths.add(frame.abs_path);
147+
}
148+
});
149+
});
150+
} catch (e) {
151+
// To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
152+
}
153+
154+
// Fill debug_meta information
155+
event.debug_meta = event.debug_meta || {};
156+
event.debug_meta.images = event.debug_meta.images || [];
157+
const images = event.debug_meta.images;
158+
errorAbsPaths.forEach(absPath => {
159+
if (absPathDebugIdMap[absPath]) {
160+
images.push({
161+
type: 'sourcemap',
162+
code_file: absPath,
163+
debug_id: absPathDebugIdMap[absPath],
164+
});
165+
}
166+
});
167+
}
168+
115169
/**
116170
* This function adds all used integrations to the SDK info in the event.
117171
* @param event The event that will be filled with all integrations.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Event } from '@sentry/types';
2+
import { createStackParser, GLOBAL_OBJ } from '@sentry/utils';
3+
4+
import { applyDebugMetadata } from '../../src/utils/prepareEvent';
5+
6+
describe('applyDebugMetadata', () => {
7+
afterEach(() => {
8+
GLOBAL_OBJ._sentryDebugIds = undefined;
9+
});
10+
11+
it('should put debug source map images in debug_meta field', () => {
12+
GLOBAL_OBJ._sentryDebugIds = {
13+
'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
14+
'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
15+
'filename4.js\nfilename4.js': 'cccccccc-cccc-4ccc-cccc-cccccccccc',
16+
};
17+
18+
const stackParser = createStackParser([0, line => ({ filename: line, abs_path: line })]);
19+
20+
const event: Event = {
21+
exception: {
22+
values: [
23+
{
24+
stacktrace: {
25+
frames: [
26+
{ abs_path: 'filename1.js', filename: 'filename1.js' },
27+
{ abs_path: 'filename2.js', filename: 'filename2.js' },
28+
{ abs_path: 'filename1.js', filename: 'filename1.js' },
29+
{ abs_path: 'filename3.js', filename: 'filename3.js' },
30+
],
31+
},
32+
},
33+
],
34+
},
35+
};
36+
37+
applyDebugMetadata(event, stackParser);
38+
39+
expect(event.debug_meta?.images).toContainEqual({
40+
type: 'sourcemap',
41+
code_file: 'filename1.js',
42+
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
43+
});
44+
45+
expect(event.debug_meta?.images).toContainEqual({
46+
type: 'sourcemap',
47+
code_file: 'filename2.js',
48+
debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
49+
});
50+
51+
// expect not to contain an image for the stack frame that doesn't have a corresponding debug id
52+
expect(event.debug_meta?.images).not.toContainEqual(
53+
expect.objectContaining({
54+
type: 'sourcemap',
55+
code_file: 'filename3.js',
56+
}),
57+
);
58+
59+
// expect not to contain an image for the debug id mapping that isn't contained in the stack trace
60+
expect(event.debug_meta?.images).not.toContainEqual(
61+
expect.objectContaining({
62+
type: 'sourcemap',
63+
code_file: 'filename4.js',
64+
debug_id: 'cccccccc-cccc-4ccc-cccc-cccccccccc',
65+
}),
66+
);
67+
});
68+
});

packages/types/src/debugMeta.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@ export interface DebugMeta {
55
images?: Array<DebugImage>;
66
}
77

8-
/**
9-
* Possible choices for debug images.
10-
*/
11-
export type DebugImageType = 'wasm' | 'macho' | 'elf' | 'pe';
8+
export type DebugImage = WasmDebugImage | SourceMapDebugImage;
129

13-
/**
14-
* References to debug images.
15-
*/
16-
export interface DebugImage {
17-
type: DebugImageType;
10+
interface WasmDebugImage {
11+
type: 'wasm';
1812
debug_id: string;
1913
code_id?: string | null;
2014
code_file: string;
2115
debug_file?: string | null;
2216
}
17+
18+
interface SourceMapDebugImage {
19+
type: 'sourcemap';
20+
code_file: string; // abs_path
21+
debug_id: string; // uuid
22+
}

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type { ClientReport, Outcome, EventDropReason } from './clientreport';
55
export type { Context, Contexts, DeviceContext, OsContext, AppContext, CultureContext, TraceContext } from './context';
66
export type { DataCategory } from './datacategory';
77
export type { DsnComponents, DsnLike, DsnProtocol } from './dsn';
8-
export type { DebugImage, DebugImageType, DebugMeta } from './debugMeta';
8+
export type { DebugImage, DebugMeta } from './debugMeta';
99
export type {
1010
AttachmentItem,
1111
BaseEnvelopeHeaders,

packages/utils/src/stacktrace.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
import type { StackFrame, StackLineParser, StackLineParserFn, StackParser } from '@sentry/types';
22

3-
import { GLOBAL_OBJ } from './worldwide';
4-
53
const STACKTRACE_LIMIT = 50;
64

7-
type DebugIdFilename = string;
8-
type DebugId = string;
9-
10-
const debugIdParserCache = new Map<StackLineParserFn, Map<DebugIdFilename, DebugId>>();
11-
125
/**
136
* Creates a stack parser with the supplied line parsers
147
*
@@ -21,29 +14,6 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser {
2114

2215
return (stack: string, skipFirst: number = 0): StackFrame[] => {
2316
const frames: StackFrame[] = [];
24-
25-
for (const parser of sortedParsers) {
26-
let debugIdCache = debugIdParserCache.get(parser);
27-
if (!debugIdCache) {
28-
debugIdCache = new Map();
29-
debugIdParserCache.set(parser, debugIdCache);
30-
}
31-
32-
const debugIdMap = GLOBAL_OBJ._sentryDebugIds;
33-
34-
if (debugIdMap) {
35-
Object.keys(debugIdMap).forEach(debugIdStackTrace => {
36-
debugIdStackTrace.split('\n').forEach(line => {
37-
const frame = parser(line);
38-
if (frame && frame.filename) {
39-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
40-
debugIdCache!.set(frame.filename, debugIdMap[debugIdStackTrace]);
41-
}
42-
});
43-
});
44-
}
45-
}
46-
4717
for (const line of stack.split('\n').slice(skipFirst)) {
4818
// Ignore lines over 1kb as they are unlikely to be stack frames.
4919
// 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 {
6131
const frame = parser(cleanedLine);
6232

6333
if (frame) {
64-
const debugIdCache = debugIdParserCache.get(parser);
65-
if (debugIdCache && frame.filename) {
66-
const cachedDebugId = debugIdCache.get(frame.filename);
67-
if (cachedDebugId) {
68-
frame.debug_id = cachedDebugId;
69-
}
70-
}
71-
7234
frames.push(frame);
7335
break;
7436
}

packages/utils/src/worldwide.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ export interface InternalGlobal {
2929
id?: string;
3030
};
3131
SENTRY_SDK_SOURCE?: SdkSource;
32+
/**
33+
* Debug IDs are indirectly injected by Sentry CLI or bundler plugins to directly reference a particular source map
34+
* for resolving of a source file. The injected code will place an entry into the record for each loaded bundle/JS
35+
* file.
36+
*/
3237
_sentryDebugIds?: Record<string, string>;
3338
__SENTRY__: {
3439
globalEventProcessors: any;

packages/utils/test/stacktrace.test.ts

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createStackParser, stripSentryFramesAndReverse } from '../src/stacktrace';
2-
import { GLOBAL_OBJ } from '../src/worldwide';
1+
import { stripSentryFramesAndReverse } from '../src/stacktrace';
32

43
describe('Stacktrace', () => {
54
describe('stripSentryFramesAndReverse()', () => {
@@ -69,41 +68,3 @@ describe('Stacktrace', () => {
6968
});
7069
});
7170
});
72-
73-
describe('Stack parsers created with createStackParser', () => {
74-
afterEach(() => {
75-
GLOBAL_OBJ._sentryDebugIds = undefined;
76-
});
77-
78-
it('put debug ids onto individual frames', () => {
79-
GLOBAL_OBJ._sentryDebugIds = {
80-
'filename1.js\nfilename1.js': 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
81-
'filename2.js\nfilename2.js': 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
82-
};
83-
84-
const fakeErrorStack = 'filename1.js\nfilename2.js\nfilename1.js\nfilename3.js';
85-
const stackParser = createStackParser([0, line => ({ filename: line })]);
86-
87-
const result = stackParser(fakeErrorStack);
88-
89-
expect(result[0]).toStrictEqual({ filename: 'filename3.js', function: '?' });
90-
91-
expect(result[1]).toStrictEqual({
92-
filename: 'filename1.js',
93-
function: '?',
94-
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
95-
});
96-
97-
expect(result[2]).toStrictEqual({
98-
filename: 'filename2.js',
99-
function: '?',
100-
debug_id: 'bbbbbbbb-bbbb-4bbb-bbbb-bbbbbbbbbb',
101-
});
102-
103-
expect(result[3]).toStrictEqual({
104-
filename: 'filename1.js',
105-
function: '?',
106-
debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa',
107-
});
108-
});
109-
});

packages/wasm/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export class Wasm implements Integration {
6161

6262
if (haveWasm) {
6363
event.debug_meta = event.debug_meta || {};
64-
event.debug_meta.images = getImages();
64+
event.debug_meta.images = [...(event.debug_meta.images || []), ...getImages()];
6565
}
6666

6767
return event;

packages/wasm/src/registry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function getModuleInfo(module: WebAssembly.Module): ModuleInfo {
4141
export function registerModule(module: WebAssembly.Module, url: string): void {
4242
const { buildId, debugFile } = getModuleInfo(module);
4343
if (buildId) {
44-
const oldIdx = IMAGES.findIndex(img => img.code_file === url);
44+
const oldIdx = getImage(url);
4545
if (oldIdx >= 0) {
4646
IMAGES.splice(oldIdx, 1);
4747
}
@@ -68,5 +68,7 @@ export function getImages(): Array<DebugImage> {
6868
* @param url the URL of the WebAssembly module.
6969
*/
7070
export function getImage(url: string): number {
71-
return IMAGES.findIndex(img => img.code_file === url);
71+
return IMAGES.findIndex(image => {
72+
return image.type === 'wasm' && image.code_file === url;
73+
});
7274
}

0 commit comments

Comments
 (0)