Skip to content

Commit

Permalink
feat(core): Add ModuleMetadata integration (#8475)
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
timfish authored Jul 13, 2023
1 parent 2c3066e commit 2b4121f
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export {
spanStatusfromHttpCode,
trace,
makeMultiplexedTransport,
ModuleMetadata,
} from '@sentry/core';
export type { SpanStatusType } from '@sentry/core';
export type { Span } from '@sentry/types';
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export { prepareEvent } from './utils/prepareEvent';
export { createCheckInEnvelope } from './checkin';
export { hasTracingEnabled } from './utils/hasTracingEnabled';
export { DEFAULT_ENVIRONMENT } from './constants';

export { ModuleMetadata } from './integrations/metadata';
import * as Integrations from './integrations';

export { Integrations };
57 changes: 57 additions & 0 deletions packages/core/src/integrations/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { EventItem, EventProcessor, Hub, Integration } from '@sentry/types';
import { forEachEnvelopeItem } from '@sentry/utils';

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

/**
* Adds module metadata to stack frames.
*
* Metadata can be injected by the Sentry bundler plugins using the `_experiments.moduleMetadata` config option.
*
* When this integration is added, the metadata passed to the bundler plugin is added to the stack frames of all events
* under the `module_metadata` property. This can be used to help in tagging or routing of events from different teams
* our sources
*/
export class ModuleMetadata implements Integration {
/*
* @inheritDoc
*/
public static id: string = 'ModuleMetadata';

/**
* @inheritDoc
*/
public name: string = ModuleMetadata.id;

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (processor: EventProcessor) => void, getCurrentHub: () => Hub): void {
const client = getCurrentHub().getClient();

if (!client || typeof client.on !== 'function') {
return;
}

// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
client.on('beforeEnvelope', envelope => {
forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'event') {
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;

if (event) {
stripMetadataFromStackFrames(event);
item[1] = event;
}
}
});
});

const stackParser = client.getOptions().stackParser;

addGlobalEventProcessor(event => {
addMetadataToStackFrames(stackParser, event);
return event;
});
}
}
66 changes: 66 additions & 0 deletions packages/core/test/lib/integrations/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Event } from '@sentry/types';
import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, parseEnvelope } from '@sentry/utils';
import { TextDecoder, TextEncoder } from 'util';

import { createTransport, getCurrentHub, ModuleMetadata } from '../../../src';
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';

const stackParser = createStackParser(nodeStackLineParser());

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

describe('ModuleMetadata integration', () => {
beforeEach(() => {
TestClient.sendEventCalled = undefined;
TestClient.instance = undefined;

GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {};
GLOBAL_OBJ._sentryModuleMetadata[stack] = { team: 'frontend' };
});

afterEach(() => {
jest.clearAllMocks();
});

test('Adds and removes metadata from stack frames', done => {
const options = getDefaultTestClientOptions({
dsn: 'https://username@domain/123',
enableSend: true,
stackParser,
integrations: [new ModuleMetadata()],
beforeSend: (event, _hint) => {
// copy the frames since reverse in in-place
const lastFrame = [...(event.exception?.values?.[0].stacktrace?.frames || [])].reverse()[0];
// Ensure module_metadata is populated in beforeSend callback
expect(lastFrame?.module_metadata).toEqual({ team: 'frontend' });
return event;
},
transport: () =>
createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => {
const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder());

expect(items[0][1]).toBeDefined();
const event = items[0][1] as Event;
const error = event.exception?.values?.[0];

// Ensure we're looking at the same error we threw
expect(error?.value).toEqual('Some error');

const lastFrame = [...(error?.stacktrace?.frames || [])].reverse()[0];
// Ensure the last frame is in fact for this file
expect(lastFrame?.filename).toEqual(__filename);

// Ensure module_metadata has been stripped from the event
expect(lastFrame?.module_metadata).toBeUndefined();

done();
return {};
}),
});

const client = new TestClient(options);
const hub = getCurrentHub();
hub.bindClient(client);
hub.captureException(new Error('Some error'));
});
});
11 changes: 9 additions & 2 deletions packages/core/test/mocks/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class TestClient extends BaseClient<TestClientOptions> {

// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
public eventFromException(exception: any): PromiseLike<Event> {
return resolvedSyncPromise({
const event: Event = {
exception: {
values: [
{
Expand All @@ -65,7 +65,14 @@ export class TestClient extends BaseClient<TestClientOptions> {
},
],
},
});
};

const frames = this._options.stackParser(exception.stack || '', 1);
if (frames.length && event?.exception?.values?.[0]) {
event.exception.values[0] = { ...event.exception.values[0], stacktrace: { frames } };
}

return resolvedSyncPromise(event);
}

public eventFromMessage(
Expand Down

0 comments on commit 2b4121f

Please sign in to comment.