Skip to content

Commit 2b4121f

Browse files
authored
feat(core): Add ModuleMetadata integration (#8475)
- Adds the `ModuleMetadata` integration that fetches metadata injected via bundler plugins and attaches is to the `module_metadata` property of every `StackFrame`. - This can later be used in `beforeSend` or another integration to route events depending on the metadata. - This integration is - Exported separately from `@sentry/core` (ie. not in `Integrations`) so it doesn't get included in default bundles - Exported separately from `@sentry/browser` so that it can be used without depending directly on core - Uses the `beforeEnvelope` hook to strip the `module_metadata` property from stack frames - Adds a test to ensure `module_metadata` is available in `beforeSend` and is stripped before sending
1 parent 2c3066e commit 2b4121f

File tree

5 files changed

+134
-3
lines changed

5 files changed

+134
-3
lines changed

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export {
3434
spanStatusfromHttpCode,
3535
trace,
3636
makeMultiplexedTransport,
37+
ModuleMetadata,
3738
} from '@sentry/core';
3839
export type { SpanStatusType } from '@sentry/core';
3940
export type { Span } from '@sentry/types';

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export { prepareEvent } from './utils/prepareEvent';
4646
export { createCheckInEnvelope } from './checkin';
4747
export { hasTracingEnabled } from './utils/hasTracingEnabled';
4848
export { DEFAULT_ENVIRONMENT } from './constants';
49-
49+
export { ModuleMetadata } from './integrations/metadata';
5050
import * as Integrations from './integrations';
5151

5252
export { Integrations };
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
2+
import { forEachEnvelopeItem } from '@sentry/utils';
3+
4+
import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';
5+
6+
/**
7+
* Adds module metadata to stack frames.
8+
*
9+
* Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
10+
*
11+
* When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events
12+
* under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams
13+
* our sources
14+
*/
15+
export class ModuleMetadata implements Integration {
16+
/*
17+
* @inheritDoc
18+
*/
19+
public static id: string = 'ModuleMetadata';
20+
21+
/**
22+
* @inheritDoc
23+
*/
24+
public name: string = ModuleMetadata.id;
25+
26+
/**
27+
* @inheritDoc
28+
*/
29+
public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void {
30+
const client = getCurrentHub().getClient();
31+
32+
if (!client || typeof client.on !== 'function') {
33+
return;
34+
}
35+
36+
// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
37+
client.on('beforeEnvelope', envelope => {
38+
forEachEnvelopeItem(envelope, (item, type) => {
39+
if (type === 'event') {
40+
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
41+
42+
if (event) {
43+
stripMetadataFromStackFrames(event);
44+
item[1] = event;
45+
}
46+
}
47+
});
48+
});
49+
50+
const stackParser = client.getOptions().stackParser;
51+
52+
addGlobalEventProcessor(event => {
53+
addMetadataToStackFrames(stackParser, event);
54+
return event;
55+
});
56+
}
57+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Event } from '@sentry/types';
2+
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, parseEnvelope } from '@sentry/utils';
3+
import { TextDecoder, TextEncoder } from 'util';
4+
5+
import { createTransport, getCurrentHub, ModuleMetadata } from '../../../src';
6+
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
7+
8+
const stackParser = createStackParser(nodeStackLineParser());
9+
10+
const stack = new Error().stack || '';
11+
12+
describe('ModuleMetadata integration', () => {
13+
beforeEach(() => {
14+
TestClient.sendEventCalled = undefined;
15+
TestClient.instance = undefined;
16+
17+
GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
18+
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
19+
});
20+
21+
afterEach(() => {
22+
jest.clearAllMocks();
23+
});
24+
25+
test('Adds and removes metadata from stack frames', done => {
26+
const options = getDefaultTestClientOptions({
27+
dsn: 'https://username@domain/123',
28+
enableSend: true,
29+
stackParser,
30+
integrations: [new ModuleMetadata()],
31+
beforeSend: (event, _hint) => {
32+
// copy the frames since reverse in in-place
33+
const lastFrame = [...(event.exception?.values?.[0].stacktrace?.frames || [])].reverse()[0];
34+
// Ensure module_metadata is populated in beforeSend callback
35+
expect(lastFrame?.module_metadata).toEqual({ team: 'frontend' });
36+
return event;
37+
},
38+
transport: () =>
39+
createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => {
40+
const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder());
41+
42+
expect(items[0][1]).toBeDefined();
43+
const event = items[0][1] as Event;
44+
const error = event.exception?.values?.[0];
45+
46+
// Ensure we're looking at the same error we threw
47+
expect(error?.value).toEqual('Some error');
48+
49+
const lastFrame = [...(error?.stacktrace?.frames || [])].reverse()[0];
50+
// Ensure the last frame is in fact for this file
51+
expect(lastFrame?.filename).toEqual(__filename);
52+
53+
// Ensure module_metadata has been stripped from the event
54+
expect(lastFrame?.module_metadata).toBeUndefined();
55+
56+
done();
57+
return {};
58+
}),
59+
});
60+
61+
const client = new TestClient(options);
62+
const hub = getCurrentHub();
63+
hub.bindClient(client);
64+
hub.captureException(new Error('Some error'));
65+
});
66+
});

packages/core/test/mocks/client.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class TestClient extends BaseClient<TestClientOptions> {
5454

5555
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
5656
public eventFromException(exception: any): PromiseLike<Event> {
57-
return resolvedSyncPromise({
57+
const event: Event = {
5858
exception: {
5959
values: [
6060
{
@@ -65,7 +65,14 @@ export class TestClient extends BaseClient<TestClientOptions> {
6565
},
6666
],
6767
},
68-
});
68+
};
69+
70+
const frames = this._options.stackParser(exception.stack || '', 1);
71+
if (frames.length && event?.exception?.values?.[0]) {
72+
event.exception.values[0] = { ...event.exception.values[0], stacktrace: { frames } };
73+
}
74+
75+
return resolvedSyncPromise(event);
6976
}
7077

7178
public eventFromMessage(

0 commit comments

Comments
 (0)