From 48b3801539d2a65df591959098d0ab97377ae5db Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Sep 2023 15:27:25 +0200 Subject: [PATCH 1/3] feat(core): Introduce `processEvent` hook on `Integration` --- packages/core/src/baseclient.ts | 69 +++++++++++++++++----------- packages/core/src/eventProcessors.ts | 51 ++++++++++++++++++++ packages/core/src/index.ts | 3 +- packages/core/src/integration.ts | 16 +++++-- packages/core/src/scope.ts | 63 ++----------------------- packages/types/src/client.ts | 8 ++++ packages/types/src/integration.ts | 7 +++ 7 files changed, 126 insertions(+), 91 deletions(-) create mode 100644 packages/core/src/eventProcessors.ts diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 1d1ad6aaa377..e20ea7a76bbb 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -12,6 +12,7 @@ import type { Event, EventDropReason, EventHint, + EventProcessor, Integration, IntegrationClass, Outcome, @@ -43,6 +44,7 @@ import { import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; +import { notifyEventProcessors } from './eventProcessors'; import type { IntegrationIndex } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import type { Scope } from './scope'; @@ -107,6 +109,8 @@ export abstract class BaseClient implements Client { // eslint-disable-next-line @typescript-eslint/ban-types private _hooks: Record; + private _eventProcessors: EventProcessor[]; + /** * Initializes this client instance. * @@ -119,6 +123,7 @@ export abstract class BaseClient implements Client { this._numProcessing = 0; this._outcomes = {}; this._hooks = {}; + this._eventProcessors = []; if (options.dsn) { this._dsn = makeDsn(options.dsn); @@ -280,6 +285,11 @@ export abstract class BaseClient implements Client { }); } + /** @inheritDoc */ + public addEventProcessor(eventProcessor: EventProcessor): void { + this._eventProcessors.push(eventProcessor); + } + /** * Sets up the integrations */ @@ -545,36 +555,41 @@ export abstract class BaseClient implements Client { this.emit('preprocessEvent', event, hint); - return prepareEvent(options, event, hint, scope).then(evt => { - if (evt === null) { - return evt; - } + return prepareEvent(options, event, hint, scope) + .then(evt => { + // Process client-scoped event processors + return notifyEventProcessors(this._eventProcessors, evt, hint); + }) + .then(evt => { + if (evt === null) { + return evt; + } - // If a trace context is not set on the event, we use the propagationContext set on the event to - // generate a trace context. If the propagationContext does not have a dynamic sampling context, we - // also generate one for it. - const { propagationContext } = evt.sdkProcessingMetadata || {}; - const trace = evt.contexts && evt.contexts.trace; - if (!trace && propagationContext) { - const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext; - evt.contexts = { - trace: { - trace_id, - span_id: spanId, - parent_span_id: parentSpanId, - }, - ...evt.contexts, - }; + // If a trace context is not set on the event, we use the propagationContext set on the event to + // generate a trace context. If the propagationContext does not have a dynamic sampling context, we + // also generate one for it. + const { propagationContext } = evt.sdkProcessingMetadata || {}; + const trace = evt.contexts && evt.contexts.trace; + if (!trace && propagationContext) { + const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext; + evt.contexts = { + trace: { + trace_id, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...evt.contexts, + }; - const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope); + const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope); - evt.sdkProcessingMetadata = { - dynamicSamplingContext, - ...evt.sdkProcessingMetadata, - }; - } - return evt; - }); + evt.sdkProcessingMetadata = { + dynamicSamplingContext, + ...evt.sdkProcessingMetadata, + }; + } + return evt; + }); } /** diff --git a/packages/core/src/eventProcessors.ts b/packages/core/src/eventProcessors.ts new file mode 100644 index 000000000000..4596788b9dcb --- /dev/null +++ b/packages/core/src/eventProcessors.ts @@ -0,0 +1,51 @@ +import type { Event, EventHint, EventProcessor } from '@sentry/types'; +import { getGlobalSingleton, isThenable, logger, SyncPromise } from '@sentry/utils'; + +/** + * Returns the global event processors. + */ +export function getGlobalEventProcessors(): EventProcessor[] { + return getGlobalSingleton('globalEventProcessors', () => []); +} + +/** + * Add a EventProcessor to be kept globally. + * @param callback EventProcessor to add + */ +export function addGlobalEventProcessor(callback: EventProcessor): void { + getGlobalEventProcessors().push(callback); +} + +/** + * Process an array of event processors, returning the processed event (or `null` if the event was dropped). + */ +export function notifyEventProcessors( + processors: EventProcessor[], + event: Event | null, + hint: EventHint, + index: number = 0, +): PromiseLike { + return new SyncPromise((resolve, reject) => { + const processor = processors[index]; + if (event === null || typeof processor !== 'function') { + resolve(event); + } else { + const result = processor({ ...event }, hint) as Event | null; + + __DEBUG_BUILD__ && + processor.id && + result === null && + logger.log(`Event processor "${processor.id}" dropped event`); + + if (isThenable(result)) { + void result + .then(final => notifyEventProcessors(processors, final, hint, index + 1).then(resolve)) + .then(null, reject); + } else { + void notifyEventProcessors(processors, result, hint, index + 1) + .then(resolve) + .then(null, reject); + } + } + }); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 67c28a3e3c57..21f7cab37505 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -36,7 +36,8 @@ export { } from './hub'; export { makeSession, closeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; -export { addGlobalEventProcessor, Scope } from './scope'; +export { Scope } from './scope'; +export { addGlobalEventProcessor } from './eventProcessors'; export { getEnvelopeEndpointWithUrlEncodedAuth, getReportDialogEndpoint } from './api'; export { BaseClient } from './baseclient'; export { ServerRuntimeClient } from './server-runtime-client'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index b2c8e4547ab8..aa8968edc8dc 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,8 +1,8 @@ -import type { Client, Integration, Options } from '@sentry/types'; +import type { Client, Event, EventHint, Integration, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; +import { addGlobalEventProcessor } from './eventProcessors'; import { getCurrentHub } from './hub'; -import { addGlobalEventProcessor } from './scope'; declare module '@sentry/types' { interface Integration { @@ -107,10 +107,20 @@ export function setupIntegration(client: Client, integration: Integration, integ } if (client.on && typeof integration.preprocessEvent === 'function') { - const callback = integration.preprocessEvent.bind(integration); + const callback = integration.preprocessEvent.bind(integration) as typeof integration.preprocessEvent; client.on('preprocessEvent', (event, hint) => callback(event, hint, client)); } + if (client.addEventProcessor && typeof integration.processEvent === 'function') { + const callback = integration.processEvent.bind(integration) as typeof integration.processEvent; + + const processor = Object.assign((event: Event, hint: EventHint) => callback(event, hint, client), { + id: integration.name, + }); + + client.addEventProcessor(processor); + } + __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index ef6832bc773d..ba4ccac23adb 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -22,17 +22,9 @@ import type { Transaction, User, } from '@sentry/types'; -import { - arrayify, - dateTimestampInSeconds, - getGlobalSingleton, - isPlainObject, - isThenable, - logger, - SyncPromise, - uuid4, -} from '@sentry/utils'; +import { arrayify, dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; /** @@ -525,7 +517,7 @@ export class Scope implements ScopeInterface { propagationContext: this._propagationContext, }; - return this._notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint); + return notifyEventProcessors([...getGlobalEventProcessors(), ...this._eventProcessors], event, hint); } /** @@ -559,40 +551,6 @@ export class Scope implements ScopeInterface { return this._breadcrumbs; } - /** - * This will be called after {@link applyToEvent} is finished. - */ - protected _notifyEventProcessors( - processors: EventProcessor[], - event: Event | null, - hint: EventHint, - index: number = 0, - ): PromiseLike { - return new SyncPromise((resolve, reject) => { - const processor = processors[index]; - if (event === null || typeof processor !== 'function') { - resolve(event); - } else { - const result = processor({ ...event }, hint) as Event | null; - - __DEBUG_BUILD__ && - processor.id && - result === null && - logger.log(`Event processor "${processor.id}" dropped event`); - - if (isThenable(result)) { - void result - .then(final => this._notifyEventProcessors(processors, final, hint, index + 1).then(resolve)) - .then(null, reject); - } else { - void this._notifyEventProcessors(processors, result, hint, index + 1) - .then(resolve) - .then(null, reject); - } - } - }); - } - /** * This will be called on every set call. */ @@ -629,21 +587,6 @@ export class Scope implements ScopeInterface { } } -/** - * Returns the global event processors. - */ -function getGlobalEventProcessors(): EventProcessor[] { - return getGlobalSingleton('globalEventProcessors', () => []); -} - -/** - * Add a EventProcessor to be kept globally. - * @param callback EventProcessor to add - */ -export function addGlobalEventProcessor(callback: EventProcessor): void { - getGlobalEventProcessors().push(callback); -} - function generatePropagationContext(): PropagationContext { return { traceId: uuid4(), diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 00361d0ada99..8ad048ee08c4 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -5,6 +5,7 @@ import type { DataCategory } from './datacategory'; import type { DsnComponents } from './dsn'; import type { DynamicSamplingContext, Envelope } from './envelope'; import type { Event, EventHint } from './event'; +import type { EventProcessor } from './eventprocessor'; import type { Integration, IntegrationClass } from './integration'; import type { ClientOptions } from './options'; import type { Scope } from './scope'; @@ -120,6 +121,13 @@ export interface Client { */ flush(timeout?: number): PromiseLike; + /** + * Adds an event processor that applies to any event processed by this client. + * + * TODO (v8): Make this a required method. + */ + addEventProcessor?(eventProcessor: EventProcessor): void; + /** Returns the client's instance of the given integration class, it any. */ getIntegration(integration: IntegrationClass): T | null; diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index 0c1feae65323..19df0b9e67c2 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -30,4 +30,11 @@ export interface Integration { * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. */ preprocessEvent?(event: Event, hint: EventHint | undefined, client: Client): void; + + /** + * An optional hook that allows to process an event. + * Return `null` to drop the event, or mutate the event & return it. + * This receives the client that the integration was installed for as third argument. + */ + processEvent?(event: Event, hint: EventHint | undefined, client: Client): Event | null | PromiseLike; } From c3544949ec91516bfeefdb85f4b2ec01fa5ce8be Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 14 Sep 2023 16:41:59 +0200 Subject: [PATCH 2/3] add tests --- packages/core/test/lib/integration.test.ts | 244 ++++++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 14b1697b9054..f431d30b2140 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -1,6 +1,15 @@ import type { Integration, Options } from '@sentry/types'; -import { getIntegrationsToSetup } from '../../src/integration'; +import { getIntegrationsToSetup, installedIntegrations, setupIntegration } from '../../src/integration'; +import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; + +function getTestClient(): TestClient { + return new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://username@domain/123', + }), + ); +} /** JSDoc */ class MockIntegration implements Integration { @@ -317,3 +326,236 @@ describe('getIntegrationsToSetup', () => { expect(integrations.map(i => i.name)).toEqual(['foo', 'Debug']); }); }); + +describe('setupIntegration', () => { + beforeEach(function () { + // Reset the (global!) list of installed integrations + installedIntegrations.splice(0, installedIntegrations.length); + }); + + it('works with a minimal integration', () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + } + + const client = getTestClient(); + const integrationIndex = {}; + const integration = new CustomIntegration(); + + setupIntegration(client, integration, integrationIndex); + + expect(integrationIndex).toEqual({ test: integration }); + expect(integration.setupOnce).toHaveBeenCalledTimes(1); + }); + + it('only calls setupOnce a single time', () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + } + + const client1 = getTestClient(); + const client2 = getTestClient(); + + const integrationIndex = {}; + const integration1 = new CustomIntegration(); + const integration2 = new CustomIntegration(); + const integration3 = new CustomIntegration(); + const integration4 = new CustomIntegration(); + + setupIntegration(client1, integration1, integrationIndex); + setupIntegration(client1, integration2, integrationIndex); + setupIntegration(client2, integration3, integrationIndex); + setupIntegration(client2, integration4, integrationIndex); + + expect(integrationIndex).toEqual({ test: integration4 }); + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration2.setupOnce).not.toHaveBeenCalled(); + expect(integration3.setupOnce).not.toHaveBeenCalled(); + expect(integration4.setupOnce).not.toHaveBeenCalled(); + }); + + it('binds preprocessEvent for each client', () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + preprocessEvent = jest.fn(); + } + + const client1 = getTestClient(); + const client2 = getTestClient(); + + const integrationIndex = {}; + const integration1 = new CustomIntegration(); + const integration2 = new CustomIntegration(); + const integration3 = new CustomIntegration(); + const integration4 = new CustomIntegration(); + + setupIntegration(client1, integration1, integrationIndex); + setupIntegration(client1, integration2, integrationIndex); + setupIntegration(client2, integration3, integrationIndex); + setupIntegration(client2, integration4, integrationIndex); + + expect(integrationIndex).toEqual({ test: integration4 }); + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration2.setupOnce).not.toHaveBeenCalled(); + expect(integration3.setupOnce).not.toHaveBeenCalled(); + expect(integration4.setupOnce).not.toHaveBeenCalled(); + + client1.captureEvent({ event_id: '1a' }); + client1.captureEvent({ event_id: '1b' }); + client2.captureEvent({ event_id: '2a' }); + client2.captureEvent({ event_id: '2b' }); + client2.captureEvent({ event_id: '2c' }); + + expect(integration1.preprocessEvent).toHaveBeenCalledTimes(2); + expect(integration2.preprocessEvent).toHaveBeenCalledTimes(2); + expect(integration3.preprocessEvent).toHaveBeenCalledTimes(3); + expect(integration4.preprocessEvent).toHaveBeenCalledTimes(3); + + expect(integration1.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); + expect(integration2.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); + expect(integration3.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); + expect(integration4.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); + }); + + it('allows to mutate events in preprocessEvent', async () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + preprocessEvent = jest.fn(event => { + event.event_id = 'mutated'; + }); + } + + const client = getTestClient(); + + const integrationIndex = {}; + const integration = new CustomIntegration(); + + setupIntegration(client, integration, integrationIndex); + + const sendEvent = jest.fn(); + client.sendEvent = sendEvent; + + client.captureEvent({ event_id: '1a' }); + await client.flush(); + + expect(sendEvent).toHaveBeenCalledTimes(1); + expect(sendEvent).toHaveBeenCalledWith(expect.objectContaining({ event_id: 'mutated' }), {}); + }); + + it('binds processEvent for each client', () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + processEvent = jest.fn(event => { + return event; + }); + } + + const client1 = getTestClient(); + const client2 = getTestClient(); + + const integrationIndex = {}; + const integration1 = new CustomIntegration(); + const integration2 = new CustomIntegration(); + const integration3 = new CustomIntegration(); + const integration4 = new CustomIntegration(); + + setupIntegration(client1, integration1, integrationIndex); + setupIntegration(client1, integration2, integrationIndex); + setupIntegration(client2, integration3, integrationIndex); + setupIntegration(client2, integration4, integrationIndex); + + expect(integrationIndex).toEqual({ test: integration4 }); + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration2.setupOnce).not.toHaveBeenCalled(); + expect(integration3.setupOnce).not.toHaveBeenCalled(); + expect(integration4.setupOnce).not.toHaveBeenCalled(); + + client1.captureEvent({ event_id: '1a' }); + client1.captureEvent({ event_id: '1b' }); + client2.captureEvent({ event_id: '2a' }); + client2.captureEvent({ event_id: '2b' }); + client2.captureEvent({ event_id: '2c' }); + + expect(integration1.processEvent).toHaveBeenCalledTimes(2); + expect(integration2.processEvent).toHaveBeenCalledTimes(2); + expect(integration3.processEvent).toHaveBeenCalledTimes(3); + expect(integration4.processEvent).toHaveBeenCalledTimes(3); + + expect(integration1.processEvent).toHaveBeenLastCalledWith( + expect.objectContaining({ event_id: '1b' }), + {}, + client1, + ); + expect(integration2.processEvent).toHaveBeenLastCalledWith( + expect.objectContaining({ event_id: '1b' }), + {}, + client1, + ); + expect(integration3.processEvent).toHaveBeenLastCalledWith( + expect.objectContaining({ event_id: '2c' }), + {}, + client2, + ); + expect(integration4.processEvent).toHaveBeenLastCalledWith( + expect.objectContaining({ event_id: '2c' }), + {}, + client2, + ); + }); + + it('allows to mutate events in processEvent', async () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + processEvent = jest.fn(_event => { + return { event_id: 'mutated' }; + }); + } + + const client = getTestClient(); + + const integrationIndex = {}; + const integration = new CustomIntegration(); + + setupIntegration(client, integration, integrationIndex); + + const sendEvent = jest.fn(); + client.sendEvent = sendEvent; + + client.captureEvent({ event_id: '1a' }); + await client.flush(); + + expect(sendEvent).toHaveBeenCalledTimes(1); + expect(sendEvent).toHaveBeenCalledWith(expect.objectContaining({ event_id: 'mutated' }), {}); + }); + + it('allows to drop events in processEvent', async () => { + class CustomIntegration implements Integration { + name = 'test'; + setupOnce = jest.fn(); + processEvent = jest.fn(_event => { + return null; + }); + } + + const client = getTestClient(); + + const integrationIndex = {}; + const integration = new CustomIntegration(); + + setupIntegration(client, integration, integrationIndex); + + const sendEvent = jest.fn(); + client.sendEvent = sendEvent; + + client.captureEvent({ event_id: '1a' }); + await client.flush(); + + expect(sendEvent).not.toHaveBeenCalled(); + }); +}); From f48a1641036e03a3e79e1b84bf7f11422c52f8da Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 14 Sep 2023 17:19:30 +0200 Subject: [PATCH 3/3] fix client adding --- packages/core/src/baseclient.ts | 65 ++++++++++++------------- packages/core/src/utils/prepareEvent.ts | 35 +++++++------ packages/types/src/client.ts | 7 +++ 3 files changed, 60 insertions(+), 47 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index e20ea7a76bbb..1a15c4bc37bd 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -44,7 +44,6 @@ import { import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; -import { notifyEventProcessors } from './eventProcessors'; import type { IntegrationIndex } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import type { Scope } from './scope'; @@ -285,6 +284,11 @@ export abstract class BaseClient implements Client { }); } + /** Get all installed event processors. */ + public getEventProcessors(): EventProcessor[] { + return this._eventProcessors; + } + /** @inheritDoc */ public addEventProcessor(eventProcessor: EventProcessor): void { this._eventProcessors.push(eventProcessor); @@ -555,41 +559,36 @@ export abstract class BaseClient implements Client { this.emit('preprocessEvent', event, hint); - return prepareEvent(options, event, hint, scope) - .then(evt => { - // Process client-scoped event processors - return notifyEventProcessors(this._eventProcessors, evt, hint); - }) - .then(evt => { - if (evt === null) { - return evt; - } + return prepareEvent(options, event, hint, scope, this).then(evt => { + if (evt === null) { + return evt; + } - // If a trace context is not set on the event, we use the propagationContext set on the event to - // generate a trace context. If the propagationContext does not have a dynamic sampling context, we - // also generate one for it. - const { propagationContext } = evt.sdkProcessingMetadata || {}; - const trace = evt.contexts && evt.contexts.trace; - if (!trace && propagationContext) { - const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext; - evt.contexts = { - trace: { - trace_id, - span_id: spanId, - parent_span_id: parentSpanId, - }, - ...evt.contexts, - }; + // If a trace context is not set on the event, we use the propagationContext set on the event to + // generate a trace context. If the propagationContext does not have a dynamic sampling context, we + // also generate one for it. + const { propagationContext } = evt.sdkProcessingMetadata || {}; + const trace = evt.contexts && evt.contexts.trace; + if (!trace && propagationContext) { + const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext as PropagationContext; + evt.contexts = { + trace: { + trace_id, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...evt.contexts, + }; - const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope); + const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this, scope); - evt.sdkProcessingMetadata = { - dynamicSamplingContext, - ...evt.sdkProcessingMetadata, - }; - } - return evt; - }); + evt.sdkProcessingMetadata = { + dynamicSamplingContext, + ...evt.sdkProcessingMetadata, + }; + } + return evt; + }); } /** diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 84bfd404c56f..13a164dbaf76 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -1,7 +1,8 @@ -import type { ClientOptions, Event, EventHint, StackFrame, StackParser } from '@sentry/types'; +import type { Client, ClientOptions, Event, EventHint, StackFrame, StackParser } from '@sentry/types'; import { dateTimestampInSeconds, GLOBAL_OBJ, normalize, resolvedSyncPromise, truncate, uuid4 } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; +import { notifyEventProcessors } from '../eventProcessors'; import { Scope } from '../scope'; /** @@ -26,6 +27,7 @@ export function prepareEvent( event: Event, hint: EventHint, scope?: Scope, + client?: Client, ): PromiseLike { const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = options; const prepared: Event = { @@ -74,20 +76,25 @@ export function prepareEvent( result = finalScope.applyToEvent(prepared, hint); } - return result.then(evt => { - if (evt) { - // We apply the debug_meta field only after all event processors have ran, so that if any event processors modified - // file names (e.g.the RewriteFrames integration) the filename -> debug ID relationship isn't destroyed. - // This should not cause any PII issues, since we're only moving data that is already on the event and not adding - // any new data - applyDebugMeta(evt); - } + return result + .then(evt => { + // Process client-scoped event processors + return client && client.getEventProcessors ? notifyEventProcessors(client.getEventProcessors(), evt, hint) : evt; + }) + .then(evt => { + if (evt) { + // We apply the debug_meta field only after all event processors have ran, so that if any event processors modified + // file names (e.g.the RewriteFrames integration) the filename -> debug ID relationship isn't destroyed. + // This should not cause any PII issues, since we're only moving data that is already on the event and not adding + // any new data + applyDebugMeta(evt); + } - if (typeof normalizeDepth === 'number' && normalizeDepth > 0) { - return normalizeEvent(evt, normalizeDepth, normalizeMaxBreadth); - } - return evt; - }); + if (typeof normalizeDepth === 'number' && normalizeDepth > 0) { + return normalizeEvent(evt, normalizeDepth, normalizeMaxBreadth); + } + return evt; + }); } /** diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 8ad048ee08c4..1b7b78066f0c 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -128,6 +128,13 @@ export interface Client { */ addEventProcessor?(eventProcessor: EventProcessor): void; + /** + * Get all added event processors for this client. + * + * TODO (v8): Make this a required method. + */ + getEventProcessors?(): EventProcessor[]; + /** Returns the client's instance of the given integration class, it any. */ getIntegration(integration: IntegrationClass): T | null;