Skip to content

Commit 1db809b

Browse files
authored
feat(core): Add helpers to get module metadata from injected code (#8438)
1 parent 194be82 commit 1db809b

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

packages/core/src/metadata.ts

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Event, StackParser } from '@sentry/types';
2+
import { GLOBAL_OBJ } from '@sentry/utils';
3+
4+
/** Keys are source filename/url, values are metadata objects. */
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
6+
const filenameMetadataMap = new Map<string, any>();
7+
/** Set of stack strings that have already been parsed. */
8+
const parsedStacks = new Set<string>();
9+
10+
function ensureMetadataStacksAreParsed(parser: StackParser): void {
11+
if (!GLOBAL_OBJ._sentryModuleMetadata) {
12+
return;
13+
}
14+
15+
for (const stack of Object.keys(GLOBAL_OBJ._sentryModuleMetadata)) {
16+
const metadata = GLOBAL_OBJ._sentryModuleMetadata[stack];
17+
18+
if (parsedStacks.has(stack)) {
19+
continue;
20+
}
21+
22+
// Ensure this stack doesn't get parsed again
23+
parsedStacks.add(stack);
24+
25+
const frames = parser(stack);
26+
27+
// Go through the frames starting from the top of the stack and find the first one with a filename
28+
for (const frame of frames.reverse()) {
29+
if (frame.filename) {
30+
// Save the metadata for this filename
31+
filenameMetadataMap.set(frame.filename, metadata);
32+
break;
33+
}
34+
}
35+
}
36+
}
37+
38+
/**
39+
* Retrieve metadata for a specific JavaScript file URL.
40+
*
41+
* Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
42+
*/
43+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
44+
export function getMetadataForUrl(parser: StackParser, filename: string): any | undefined {
45+
ensureMetadataStacksAreParsed(parser);
46+
return filenameMetadataMap.get(filename);
47+
}
48+
49+
/**
50+
* Adds metadata to stack frames.
51+
*
52+
* Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
53+
*/
54+
export function addMetadataToStackFrames(parser: StackParser, event: Event): void {
55+
try {
56+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
57+
event.exception!.values!.forEach(exception => {
58+
if (!exception.stacktrace) {
59+
return;
60+
}
61+
62+
for (const frame of exception.stacktrace.frames || []) {
63+
if (!frame.filename) {
64+
continue;
65+
}
66+
67+
const metadata = getMetadataForUrl(parser, frame.filename);
68+
69+
if (metadata) {
70+
frame.module_metadata = metadata;
71+
}
72+
}
73+
});
74+
} catch (_) {
75+
// To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
76+
}
77+
}
78+
79+
/**
80+
* Strips metadata from stack frames.
81+
*/
82+
export function stripMetadataFromStackFrames(event: Event): void {
83+
try {
84+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85+
event.exception!.values!.forEach(exception => {
86+
if (!exception.stacktrace) {
87+
return;
88+
}
89+
90+
for (const frame of exception.stacktrace.frames || []) {
91+
delete frame.module_metadata;
92+
}
93+
});
94+
} catch (_) {
95+
// To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
96+
}
97+
}
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Event } from '@sentry/types';
2+
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser } from '@sentry/utils';
3+
4+
import { addMetadataToStackFrames, getMetadataForUrl, stripMetadataFromStackFrames } from '../../src/metadata';
5+
6+
const parser = createStackParser(nodeStackLineParser());
7+
8+
const stack = new Error().stack || '';
9+
10+
const event: Event = {
11+
exception: {
12+
values: [
13+
{
14+
stacktrace: {
15+
frames: [
16+
{
17+
filename: '<anonymous>',
18+
function: 'new Promise',
19+
},
20+
{
21+
filename: '/tmp/utils.js',
22+
function: 'Promise.then.completed',
23+
lineno: 391,
24+
colno: 28,
25+
},
26+
{
27+
filename: __filename,
28+
function: 'Object.<anonymous>',
29+
lineno: 9,
30+
colno: 19,
31+
},
32+
],
33+
},
34+
},
35+
],
36+
},
37+
};
38+
39+
describe('Metadata', () => {
40+
beforeEach(() => {
41+
GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
42+
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
43+
});
44+
45+
it('is parsed', () => {
46+
const metadata = getMetadataForUrl(parser, __filename);
47+
48+
expect(metadata).toEqual({ team: 'frontend' });
49+
});
50+
51+
it('is added and stripped from stack frames', () => {
52+
addMetadataToStackFrames(parser, event);
53+
54+
expect(event.exception?.values?.[0].stacktrace?.frames).toEqual([
55+
{
56+
filename: '<anonymous>',
57+
function: 'new Promise',
58+
},
59+
{
60+
filename: '/tmp/utils.js',
61+
function: 'Promise.then.completed',
62+
lineno: 391,
63+
colno: 28,
64+
},
65+
{
66+
filename: __filename,
67+
function: 'Object.<anonymous>',
68+
lineno: 9,
69+
colno: 19,
70+
module_metadata: {
71+
team: 'frontend',
72+
},
73+
},
74+
]);
75+
76+
stripMetadataFromStackFrames(event);
77+
78+
expect(event.exception?.values?.[0].stacktrace?.frames).toEqual([
79+
{
80+
filename: '<anonymous>',
81+
function: 'new Promise',
82+
},
83+
{
84+
filename: '/tmp/utils.js',
85+
function: 'Promise.then.completed',
86+
lineno: 391,
87+
colno: 28,
88+
},
89+
{
90+
filename: __filename,
91+
function: 'Object.<anonymous>',
92+
lineno: 9,
93+
colno: 19,
94+
},
95+
]);
96+
});
97+
});

packages/types/src/stackframe.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ export interface StackFrame {
1515
addr_mode?: string;
1616
vars?: { [key: string]: any };
1717
debug_id?: string;
18+
module_metadata?: any;
1819
}

packages/utils/src/worldwide.ts

+6
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ export interface InternalGlobal {
5555
[key: string]: Function;
5656
};
5757
};
58+
/**
59+
* Raw module metadata that is injected by bundler plugins.
60+
*
61+
* Keys are `error.stack` strings, values are the metadata.
62+
*/
63+
_sentryModuleMetadata?: Record<string, any>;
5864
}
5965

6066
// The code below for 'isGlobalObj' and 'GLOBAL_OBJ' was copied from core-js before modification

0 commit comments

Comments
 (0)