Skip to content

Commit

Permalink
feat(core): Add helpers to get module metadata from injected code (#8438
Browse files Browse the repository at this point in the history
)
  • Loading branch information
timfish authored Jul 4, 2023
1 parent 194be82 commit 1db809b
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 0 deletions.
97 changes: 97 additions & 0 deletions packages/core/src/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Event, StackParser } from '@sentry/types';
import { GLOBAL_OBJ } from '@sentry/utils';

/** Keys are source filename/url, values are metadata objects. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const filenameMetadataMap = new Map<string, any>();
/** Set of stack strings that have already been parsed. */
const parsedStacks = new Set<string>();

function ensureMetadataStacksAreParsed(parser: StackParser): void {
if (!GLOBAL_OBJ._sentryModuleMetadata) {
return;
}

for (const stack of Object.keys(GLOBAL_OBJ._sentryModuleMetadata)) {
const metadata = GLOBAL_OBJ._sentryModuleMetadata[stack];

if (parsedStacks.has(stack)) {
continue;
}

// Ensure this stack doesn't get parsed again
parsedStacks.add(stack);

const frames = parser(stack);

// Go through the frames starting from the top of the stack and find the first one with a filename
for (const frame of frames.reverse()) {
if (frame.filename) {
// Save the metadata for this filename
filenameMetadataMap.set(frame.filename, metadata);
break;
}
}
}
}

/**
* Retrieve metadata for a specific JavaScript file URL.
*
* Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getMetadataForUrl(parser: StackParser, filename: string): any | undefined {
ensureMetadataStacksAreParsed(parser);
return filenameMetadataMap.get(filename);
}

/**
* Adds metadata to stack frames.
*
* Metadata is injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
*/
export function addMetadataToStackFrames(parser: StackParser, event: Event): void {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
event.exception!.values!.forEach(exception => {
if (!exception.stacktrace) {
return;
}

for (const frame of exception.stacktrace.frames || []) {
if (!frame.filename) {
continue;
}

const metadata = getMetadataForUrl(parser, frame.filename);

if (metadata) {
frame.module_metadata = metadata;
}
}
});
} catch (_) {
// To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
}
}

/**
* Strips metadata from stack frames.
*/
export function stripMetadataFromStackFrames(event: Event): void {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
event.exception!.values!.forEach(exception => {
if (!exception.stacktrace) {
return;
}

for (const frame of exception.stacktrace.frames || []) {
delete frame.module_metadata;
}
});
} catch (_) {
// To save bundle size we're just try catching here instead of checking for the existence of all the different objects.
}
}
97 changes: 97 additions & 0 deletions packages/core/test/lib/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Event } from '@sentry/types';
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser } from '@sentry/utils';

import { addMetadataToStackFrames, getMetadataForUrl, stripMetadataFromStackFrames } from '../../src/metadata';

const parser = createStackParser(nodeStackLineParser());

const stack = new Error().stack || '';

const event: Event = {
exception: {
values: [
{
stacktrace: {
frames: [
{
filename: '<anonymous>',
function: 'new Promise',
},
{
filename: '/tmp/utils.js',
function: 'Promise.then.completed',
lineno: 391,
colno: 28,
},
{
filename: __filename,
function: 'Object.<anonymous>',
lineno: 9,
colno: 19,
},
],
},
},
],
},
};

describe('Metadata', () => {
beforeEach(() => {
GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
});

it('is parsed', () => {
const metadata = getMetadataForUrl(parser, __filename);

expect(metadata).toEqual({ team: 'frontend' });
});

it('is added and stripped from stack frames', () => {
addMetadataToStackFrames(parser, event);

expect(event.exception?.values?.[0].stacktrace?.frames).toEqual([
{
filename: '<anonymous>',
function: 'new Promise',
},
{
filename: '/tmp/utils.js',
function: 'Promise.then.completed',
lineno: 391,
colno: 28,
},
{
filename: __filename,
function: 'Object.<anonymous>',
lineno: 9,
colno: 19,
module_metadata: {
team: 'frontend',
},
},
]);

stripMetadataFromStackFrames(event);

expect(event.exception?.values?.[0].stacktrace?.frames).toEqual([
{
filename: '<anonymous>',
function: 'new Promise',
},
{
filename: '/tmp/utils.js',
function: 'Promise.then.completed',
lineno: 391,
colno: 28,
},
{
filename: __filename,
function: 'Object.<anonymous>',
lineno: 9,
colno: 19,
},
]);
});
});
1 change: 1 addition & 0 deletions packages/types/src/stackframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export interface StackFrame {
addr_mode?: string;
vars?: { [key: string]: any };
debug_id?: string;
module_metadata?: any;
}
6 changes: 6 additions & 0 deletions packages/utils/src/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export interface InternalGlobal {
[key: string]: Function;
};
};
/**
* Raw module metadata that is injected by bundler plugins.
*
* Keys are `error.stack` strings, values are the metadata.
*/
_sentryModuleMetadata?: Record<string, any>;
}

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

0 comments on commit 1db809b

Please sign in to comment.