Skip to content

feat(core): Add ModuleMetadata integration #8475

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 13, 2023
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