diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index cfa46af6a058..0b54fea6212c 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -637,7 +637,7 @@ export abstract class BaseClient implements Client { .then(prepared => { if (prepared === null) { this.recordDroppedEvent('event_processor', event.type || 'error'); - throw new SentryError('An event processor returned null, will not send event.', 'log'); + throw new SentryError('An event processor returned `null`, will not send event.', 'log'); } const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true; @@ -646,7 +646,7 @@ export abstract class BaseClient implements Client { } const beforeSendResult = beforeSend(prepared, hint); - return _ensureBeforeSendRv(beforeSendResult); + return _validateBeforeSendResult(beforeSendResult); }) .then(processedEvent => { if (processedEvent === null) { @@ -764,15 +764,17 @@ export abstract class BaseClient implements Client { } /** - * Verifies that return value of configured `beforeSend` is of expected type. + * Verifies that return value of configured `beforeSend` is of expected type, and returns the value if so. */ -function _ensureBeforeSendRv(rv: PromiseLike | Event | null): PromiseLike | Event | null { - const nullErr = '`beforeSend` method has to return `null` or a valid event.'; - if (isThenable(rv)) { - return rv.then( +function _validateBeforeSendResult( + beforeSendResult: PromiseLike | Event | null, +): PromiseLike | Event | null { + const invalidValueError = '`beforeSend` must return `null` or a valid event.'; + if (isThenable(beforeSendResult)) { + return beforeSendResult.then( event => { - if (!(isPlainObject(event) || event === null)) { - throw new SentryError(nullErr); + if (!isPlainObject(event) && event !== null) { + throw new SentryError(invalidValueError); } return event; }, @@ -780,8 +782,8 @@ function _ensureBeforeSendRv(rv: PromiseLike | Event | null): Prom throw new SentryError(`beforeSend rejected with ${e}`); }, ); - } else if (!(isPlainObject(rv) || rv === null)) { - throw new SentryError(nullErr); + } else if (!isPlainObject(beforeSendResult) && beforeSendResult !== null) { + throw new SentryError(invalidValueError); } - return rv; + return beforeSendResult; } diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index beb4d412a2f7..4518613509d4 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -66,7 +66,7 @@ describe('BaseClient', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); - expect(dsnToString(client.getDsn()!)).toBe(PUBLIC_DSN); + expect(dsnToString(client.getDsn()!)).toEqual(PUBLIC_DSN); }); test('allows missing Dsn', () => { @@ -120,7 +120,7 @@ describe('BaseClient', () => { scope.addBreadcrumb({ message: 'hello' }, 100); hub.addBreadcrumb({ message: 'world' }); - expect((scope as any)._breadcrumbs[1].message).toBe('world'); + expect((scope as any)._breadcrumbs[1].message).toEqual('world'); }); test('adds a timestamp to new breadcrumbs', () => { @@ -137,7 +137,7 @@ describe('BaseClient', () => { expect((scope as any)._breadcrumbs[1].timestamp).toBeGreaterThan(1); }); - test('discards breadcrumbs beyond maxBreadcrumbs', () => { + test('discards breadcrumbs beyond `maxBreadcrumbs`', () => { expect.assertions(2); const options = getDefaultTestClientOptions({ maxBreadcrumbs: 1 }); @@ -148,8 +148,8 @@ describe('BaseClient', () => { scope.addBreadcrumb({ message: 'hello' }, 100); hub.addBreadcrumb({ message: 'world' }); - expect((scope as any)._breadcrumbs.length).toBe(1); - expect((scope as any)._breadcrumbs[0].message).toBe('world'); + expect((scope as any)._breadcrumbs.length).toEqual(1); + expect((scope as any)._breadcrumbs[0].message).toEqual('world'); }); test('allows concurrent updates', () => { @@ -166,7 +166,7 @@ describe('BaseClient', () => { expect((scope as any)._breadcrumbs).toHaveLength(2); }); - test('calls beforeBreadcrumb and adds the breadcrumb without any changes', () => { + test('calls `beforeBreadcrumb` and adds the breadcrumb without any changes', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(breadcrumb => breadcrumb); @@ -177,10 +177,10 @@ describe('BaseClient', () => { hub.addBreadcrumb({ message: 'hello' }); - expect((scope as any)._breadcrumbs[0].message).toBe('hello'); + expect((scope as any)._breadcrumbs[0].message).toEqual('hello'); }); - test('calls beforeBreadcrumb and uses the new one', () => { + test('calls `beforeBreadcrumb` and uses the new one', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => ({ message: 'changed' })); @@ -191,10 +191,10 @@ describe('BaseClient', () => { hub.addBreadcrumb({ message: 'hello' }); - expect((scope as any)._breadcrumbs[0].message).toBe('changed'); + expect((scope as any)._breadcrumbs[0].message).toEqual('changed'); }); - test('calls beforeBreadcrumb and discards the breadcrumb when returned null', () => { + test('calls `beforeBreadcrumb` and discards the breadcrumb when returned `null`', () => { expect.assertions(1); const beforeBreadcrumb = jest.fn(() => null); @@ -205,10 +205,10 @@ describe('BaseClient', () => { hub.addBreadcrumb({ message: 'hello' }); - expect((scope as any)._breadcrumbs.length).toBe(0); + expect((scope as any)._breadcrumbs.length).toEqual(0); }); - test('calls beforeBreadcrumb gets an access to a hint as a second argument', () => { + test('`beforeBreadcrumb` gets an access to a hint as a second argument', () => { expect.assertions(2); const beforeBreadcrumb = jest.fn((breadcrumb, hint) => ({ ...breadcrumb, data: hint.data })); @@ -219,8 +219,8 @@ describe('BaseClient', () => { hub.addBreadcrumb({ message: 'hello' }, { data: 'someRandomThing' }); - expect((scope as any)._breadcrumbs[0].message).toBe('hello'); - expect((scope as any)._breadcrumbs[0].data).toBe('someRandomThing'); + expect((scope as any)._breadcrumbs[0].message).toEqual('hello'); + expect((scope as any)._breadcrumbs[0].data).toEqual('someRandomThing'); }); }); @@ -347,7 +347,7 @@ describe('BaseClient', () => { ); }); - test('should call eventFromException if input to captureMessage is not a primitive', () => { + test('should call `eventFromException` if input to `captureMessage` is not a primitive', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); const spy = jest.spyOn(TestClient.instance!, 'eventFromException'); @@ -461,7 +461,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message' }, undefined, scope); - expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event!.message).toEqual('message'); expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', @@ -481,7 +481,7 @@ describe('BaseClient', () => { client.captureEvent({ message: 'message', timestamp: 1234 }, undefined, scope); - expect(TestClient.instance!.event!.message).toBe('message'); + expect(TestClient.instance!.event!.message).toEqual('message'); expect(TestClient.instance!.event).toEqual( expect.objectContaining({ environment: 'production', @@ -492,7 +492,7 @@ describe('BaseClient', () => { ); }); - test('adds event_id from hint if available', () => { + test('adds `event_id` from hint if available', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); @@ -887,7 +887,7 @@ describe('BaseClient', () => { expect(capturedEvent).toEqual(normalizedTransaction); }); - test('calls beforeSend and uses original event without any changes', () => { + test('calls `beforeSend` and uses original event without any changes', () => { expect.assertions(1); const beforeSend = jest.fn(event => event); @@ -896,22 +896,25 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); - expect(TestClient.instance!.event!.message).toBe('hello'); + expect(TestClient.instance!.event!.message).toEqual('hello'); }); - test('calls beforeSend and uses the new one', () => { + test('calls `beforeSend` and uses the modified event', () => { expect.assertions(1); - const beforeSend = jest.fn(() => ({ message: 'changed1' })); + const beforeSend = jest.fn(event => { + event.message = 'changed1'; + return event; + }); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options); client.captureEvent({ message: 'hello' }); - expect(TestClient.instance!.event!.message).toBe('changed1'); + expect(TestClient.instance!.event!.message).toEqual('changed1'); }); - test('calls beforeSend and discards the event', () => { + test('calls `beforeSend` and discards the event', () => { expect.assertions(3); const beforeSend = jest.fn(() => null); @@ -923,11 +926,13 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); expect(TestClient.instance!.event).toBeUndefined(); + // This proves that the reason the event didn't send/didn't get set on the test client is not because there was an + // error, but because `beforeSend` returned `null` expect(captureExceptionSpy).not.toBeCalled(); expect(loggerWarnSpy).toBeCalledWith('`beforeSend` returned `null`, will not send event.'); }); - test('calls beforeSend and log info about invalid return value', () => { + test('calls `beforeSend` and logs info about invalid return value', () => { const invalidValues = [undefined, false, true, [], 1]; expect.assertions(invalidValues.length * 2); @@ -941,13 +946,11 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }); expect(TestClient.instance!.event).toBeUndefined(); - expect(loggerWarnSpy).toBeCalledWith( - new SentryError('`beforeSend` method has to return `null` or a valid event.'), - ); + expect(loggerWarnSpy).toBeCalledWith(new SentryError('`beforeSend` must return `null` or a valid event.')); } }); - test('calls async beforeSend and uses original event without any changes', done => { + test('calls async `beforeSend` and uses original event without any changes', done => { jest.useFakeTimers(); expect.assertions(1); @@ -966,7 +969,7 @@ describe('BaseClient', () => { jest.runOnlyPendingTimers(); TestClient.sendEventCalled = (event: Event) => { - expect(event.message).toBe('hello'); + expect(event.message).toEqual('hello'); }; setTimeout(() => { @@ -976,18 +979,18 @@ describe('BaseClient', () => { jest.runOnlyPendingTimers(); }); - test('calls async beforeSend and uses the new one', done => { + test('calls async `beforeSend` and uses the modified event', done => { jest.useFakeTimers(); expect.assertions(1); - const beforeSend = jest.fn( - async () => - new Promise(resolve => { - setTimeout(() => { - resolve({ message: 'changed2' }); - }, 1); - }), - ); + const beforeSend = jest.fn(async event => { + event.message = 'changed2'; + return new Promise(resolve => { + setTimeout(() => { + resolve(event); + }, 1); + }); + }); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options); @@ -995,7 +998,7 @@ describe('BaseClient', () => { jest.runOnlyPendingTimers(); TestClient.sendEventCalled = (event: Event) => { - expect(event.message).toBe('changed2'); + expect(event.message).toEqual('changed2'); }; setTimeout(() => { @@ -1005,7 +1008,7 @@ describe('BaseClient', () => { jest.runOnlyPendingTimers(); }); - test('calls async beforeSend and discards the event', () => { + test('calls async `beforeSend` and discards the event', () => { jest.useFakeTimers(); expect.assertions(1); @@ -1026,7 +1029,7 @@ describe('BaseClient', () => { expect(TestClient.instance!.event).toBeUndefined(); }); - test('beforeSend gets access to a hint as a second argument', () => { + test('`beforeSend` gets access to a hint as a second argument', () => { expect.assertions(2); const beforeSend = jest.fn((event, hint) => ({ ...event, data: hint.data })); @@ -1035,11 +1038,11 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, { data: 'someRandomThing' }); - expect(TestClient.instance!.event!.message).toBe('hello'); - expect((TestClient.instance!.event! as any).data).toBe('someRandomThing'); + expect(TestClient.instance!.event!.message).toEqual('hello'); + expect((TestClient.instance!.event! as any).data).toEqual('someRandomThing'); }); - test('beforeSend records dropped events', () => { + test('`beforeSend` records dropped events', () => { expect.assertions(1); const client = new TestClient( @@ -1058,7 +1061,7 @@ describe('BaseClient', () => { expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'error'); }); - test('eventProcessor can drop the even when it returns null', () => { + test('event processor drops the event when it returns `null`', () => { expect.assertions(3); const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN })); @@ -1070,11 +1073,13 @@ describe('BaseClient', () => { client.captureEvent({ message: 'hello' }, {}, scope); expect(TestClient.instance!.event).toBeUndefined(); + // This proves that the reason the event didn't send/didn't get set on the test client is not because there was an + // error, but because the event processor returned `null` expect(captureExceptionSpy).not.toBeCalled(); - expect(loggerLogSpy).toBeCalledWith('An event processor returned null, will not send event.'); + expect(loggerLogSpy).toBeCalledWith('An event processor returned `null`, will not send event.'); }); - test('eventProcessor records dropped events', () => { + test('event processor records dropped events', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); @@ -1090,24 +1095,12 @@ describe('BaseClient', () => { expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'error'); }); - test('mutating transaction name with event processors sets transaction name change metadata', () => { + test('mutating transaction name with event processors sets transaction-name-change metadata', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, enableSend: true }); const client = new TestClient(options); const transaction: Event = { - contexts: { - trace: { - op: 'pageload', - span_id: 'a3df84a60c2e4e76', - trace_id: '86f39e84263a4de99c326acab3bfe3bd', - }, - }, - environment: 'production', - event_id: '972f45b826a248bba98e990878a177e1', - spans: [], - start_timestamp: 1591603196.614865, - timestamp: 1591603196.728485, - transaction: 'initialName', + transaction: '/dogs/are/great', type: 'transaction', transaction_info: { source: 'url', @@ -1118,12 +1111,12 @@ describe('BaseClient', () => { const scope = new Scope(); scope.addEventProcessor(event => { - event.transaction = 'updatedName'; + event.transaction = '/adopt/dont/shop'; return event; }); client.captureEvent(transaction, {}, scope); - expect(TestClient.instance!.event!.transaction).toEqual('updatedName'); + expect(TestClient.instance!.event!.transaction).toEqual('/adopt/dont/shop'); expect(TestClient.instance!.event!.transaction_info).toEqual({ source: 'custom', changes: [ @@ -1137,7 +1130,7 @@ describe('BaseClient', () => { }); }); - test('eventProcessor sends an event and logs when it crashes', () => { + test('event processor sends an event and logs when it crashes', () => { expect.assertions(3); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); @@ -1166,7 +1159,7 @@ describe('BaseClient', () => { ); }); - test('records events dropped due to sampleRate', () => { + test('records events dropped due to `sampleRate` option', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, sampleRate: 0 }); @@ -1184,14 +1177,14 @@ describe('BaseClient', () => { global.__SENTRY__ = {}; }); - test('setup each one of them on setupIntegration call', () => { + test('sets up each integration on `setupIntegrations` call', () => { expect.assertions(2); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, integrations: [new TestIntegration()] }); const client = new TestClient(options); client.setupIntegrations(); - expect(Object.keys((client as any)._integrations).length).toBe(1); + expect(Object.keys((client as any)._integrations).length).toEqual(1); expect(client.getIntegration(TestIntegration)).toBeTruthy(); }); @@ -1202,11 +1195,11 @@ describe('BaseClient', () => { const client = new TestClient(options); client.setupIntegrations(); - expect(Object.keys((client as any)._integrations).length).toBe(0); + expect(Object.keys((client as any)._integrations).length).toEqual(0); expect(client.getIntegration(TestIntegration)).toBeFalsy(); }); - test('skips installation if enabled is set to false', () => { + test('skips installation if `enabled` is set to `false`', () => { expect.assertions(2); const options = getDefaultTestClientOptions({ @@ -1217,7 +1210,7 @@ describe('BaseClient', () => { const client = new TestClient(options); client.setupIntegrations(); - expect(Object.keys((client as any)._integrations).length).toBe(0); + expect(Object.keys((client as any)._integrations).length).toEqual(0); expect(client.getIntegration(TestIntegration)).toBeFalsy(); }); @@ -1232,7 +1225,7 @@ describe('BaseClient', () => { // it should install the first time, because integrations aren't yet installed... client.setupIntegrations(); - expect(Object.keys((client as any)._integrations).length).toBe(1); + expect(Object.keys((client as any)._integrations).length).toEqual(1); expect(client.getIntegration(TestIntegration)).toBeTruthy(); expect(setupIntegrationsHelper).toHaveBeenCalledTimes(1); @@ -1373,7 +1366,7 @@ describe('BaseClient', () => { }); describe('recordDroppedEvent()/_clearOutcomes()', () => { - test('record and return outcomes', () => { + test('records and returns outcomes', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); @@ -1381,7 +1374,7 @@ describe('BaseClient', () => { client.recordDroppedEvent('ratelimit_backoff', 'error'); client.recordDroppedEvent('network_error', 'transaction'); client.recordDroppedEvent('network_error', 'transaction'); - client.recordDroppedEvent('before_send', 'session'); + client.recordDroppedEvent('before_send', 'error'); client.recordDroppedEvent('event_processor', 'attachment'); client.recordDroppedEvent('network_error', 'transaction'); @@ -1401,7 +1394,7 @@ describe('BaseClient', () => { }, { reason: 'before_send', - category: 'session', + category: 'error', quantity: 1, }, { @@ -1413,7 +1406,7 @@ describe('BaseClient', () => { ); }); - test('to clear outcomes', () => { + test('clears outcomes', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); const client = new TestClient(options); diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts index b0affcada15b..ec69c9e4c81a 100644 --- a/packages/core/test/lib/hint.test.ts +++ b/packages/core/test/lib/hint.test.ts @@ -20,7 +20,7 @@ describe('Hint', () => { }); describe('attachments', () => { - test('can be mutated in beforeSend', () => { + test('can be mutated in `beforeSend`', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ @@ -38,7 +38,7 @@ describe('Hint', () => { expect(hint).toEqual({ attachments: [{ filename: 'another.file', data: 'more text' }] }); }); - test('gets passed through to beforeSend and can be further mutated', () => { + test('gets passed through to `beforeSend` and can be further mutated', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ @@ -76,7 +76,7 @@ describe('Hint', () => { expect(hint?.attachments).toEqual([{ filename: 'integration.file', data: 'great content!' }]); }); - test('get copied from scope to hint', () => { + test('gets copied from scope to hint', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); diff --git a/packages/nextjs/test/index.client.test.ts b/packages/nextjs/test/index.client.test.ts index 699b2fac1f0a..8b61ef7ff1ba 100644 --- a/packages/nextjs/test/index.client.test.ts +++ b/packages/nextjs/test/index.client.test.ts @@ -93,7 +93,7 @@ describe('Client init()', () => { expect(transportSend).not.toHaveBeenCalled(); expect(captureEvent.mock.results[0].value).toBeUndefined(); - expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned null, will not send event.'); + expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned `null`, will not send event.'); }); describe('integrations', () => { diff --git a/packages/nextjs/test/index.server.test.ts b/packages/nextjs/test/index.server.test.ts index 08d4178deb5b..d432b61eb16e 100644 --- a/packages/nextjs/test/index.server.test.ts +++ b/packages/nextjs/test/index.server.test.ts @@ -111,7 +111,7 @@ describe('Server init()', () => { await SentryNode.flush(); expect(transportSend).not.toHaveBeenCalled(); - expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned null, will not send event.'); + expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned `null`, will not send event.'); }); it("initializes both global hub and domain hub when there's an active domain", () => { diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 7e5fc929beea..3cbd6ebf2922 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -75,14 +75,16 @@ describe('SentryNode', () => { }); describe('breadcrumbs', () => { - let s: jest.SpyInstance; + let sendEventSpy: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + sendEventSpy = jest + .spyOn(NodeClient.prototype, 'sendEvent') + .mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { - s.mockRestore(); + sendEventSpy.mockRestore(); }); test('record auto breadcrumbs', done => { @@ -106,14 +108,16 @@ describe('SentryNode', () => { }); describe('capture', () => { - let s: jest.SpyInstance; + let sendEventSpy: jest.SpyInstance; beforeEach(() => { - s = jest.spyOn(NodeClient.prototype, 'sendEvent').mockImplementation(async () => Promise.resolve({ code: 200 })); + sendEventSpy = jest + .spyOn(NodeClient.prototype, 'sendEvent') + .mockImplementation(async () => Promise.resolve({ code: 200 })); }); afterEach(() => { - s.mockRestore(); + sendEventSpy.mockRestore(); }); test('capture an exception', done => { @@ -359,7 +363,7 @@ describe('SentryNode initialization', () => { }); describe('SDK metadata', () => { - it('should set SDK data when Sentry.init() is called', () => { + it('should set SDK data when `Sentry.init()` is called', () => { init({ dsn }); // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -435,7 +439,7 @@ describe('SentryNode initialization', () => { }); }); - it('should ignore autoloaded integrations when defaultIntegrations:false', () => { + it('should ignore autoloaded integrations when `defaultIntegrations` is `false`', () => { withAutoloadedIntegrations([new MockIntegration('foo')], () => { init({ defaultIntegrations: false, diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index e4641b97aab1..efd4812b7efb 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -195,15 +195,14 @@ export interface ClientOptions number | boolean; /** - * A callback invoked during event submission, allowing to optionally modify - * the event before it is sent to Sentry. + * An event-processing callback for error and message events, guaranteed to be invoked after all other event + * processors, which allows an event to be modified or dropped. * - * Note that you must return a valid event from this callback. If you do not - * wish to modify the event, simply return it at the end. - * Returning null will cause the event to be dropped. + * Note that you must return a valid event from this callback. If you do not wish to modify the event, simply return + * it at the end. Returning `null` will cause the event to be dropped. * * @param event The error or message event generated by the SDK. - * @param hint May contain additional information about the original exception. + * @param hint Event metadata useful for processing. * @returns A new event that will be sent | null. */ beforeSend?: (event: Event, hint: EventHint) => PromiseLike | Event | null;