From 6d44bc8fed4e652847c41856e507008a55db3c78 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 24 Jan 2024 15:54:46 -0300 Subject: [PATCH] add client report support --- src/client.ts | 34 +++++++--- src/utils/outcome.ts | 24 +++++++ test/client.test.ts | 87 +++++++++++++----------- test/utils/outcomes.test.ts | 129 ++++++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+), 48 deletions(-) create mode 100644 src/utils/outcome.ts create mode 100644 test/utils/outcomes.test.ts diff --git a/src/client.ts b/src/client.ts index a6055eac..c5fc1e6b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,6 +1,8 @@ import { eventFromException, eventFromMessage } from '@sentry/browser'; import { BaseClient } from '@sentry/core'; import type { + ClientReportEnvelope, + ClientReportItem, Envelope, Event, EventHint, @@ -10,11 +12,14 @@ import type { Thread, UserFeedback, } from '@sentry/types'; -import { logger, SentryError } from '@sentry/utils'; +import { dateTimestampInSeconds, logger, SentryError } from '@sentry/utils'; import type { CapacitorClientOptions } from './options'; +import { mergeOutcomes } from './utils/outcome'; import { NATIVE } from './wrapper'; +export const EnvelopeitemsIndex = 1; + /** * The Sentry Capacitor SDK Client. * @@ -107,8 +112,7 @@ export class CapacitorClient extends BaseClient { this._outcomesBuffer = mergeOutcomes(this._outcomesBuffer, outcomes); if (this._options.sendClientReports) { - // TODO: Implement Cleint Report. - // this._attachClientReportTo(this._outcomesBuffer, envelope as ClientReportEnvelope); + this._attachClientReportTo(this._outcomesBuffer, envelope as ClientReportEnvelope); } let shouldClearOutcomesBuffer = true; @@ -162,12 +166,20 @@ export class CapacitorClient extends BaseClient { console.log('Sentry Warning, could not connect to Sentry native SDK.\nIf you do not want to use the native component please pass `enableNative: false` in the options.\nVisit: https://docs.sentry.io/platforms/javascript/guides/capacitor/configuration/options/#hybrid-sdk-options for more details.'); } } - -// TODO: implement Attaches clients report. -} -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function mergeOutcomes(_outcomesBuffer: Outcome[], outcomes: Outcome[]): Outcome[] { - // TODO: Implement mergeOutComes. - throw new Error('Function not implemented.'); + /** + * Attaches a client report from outcomes to the envelope. + */ + private _attachClientReportTo(outcomes: Outcome[], envelope: ClientReportEnvelope): void { + if (outcomes.length > 0) { + const clientReportItem: ClientReportItem = [ + { type: 'client_report' }, + { + timestamp: dateTimestampInSeconds(), + discarded_events: outcomes, + }, + ]; + + envelope[EnvelopeitemsIndex].push(clientReportItem); + } + } } - diff --git a/src/utils/outcome.ts b/src/utils/outcome.ts new file mode 100644 index 00000000..4a6dd124 --- /dev/null +++ b/src/utils/outcome.ts @@ -0,0 +1,24 @@ + import type { Outcome } from '@sentry/types'; + +/** + * Merges buffer with new outcomes. + */ +export function mergeOutcomes(...merge: Outcome[][]): Outcome[] { + let counter = 0; + const map = new Map(); + const outcomes: Outcome[] = []; + + const process = (outcome: Outcome): void => { + const key = `${outcome.reason}:${outcome.category}`; + const index = map.get(key); + if (typeof(index) !== "undefined") { + outcomes[index].quantity += outcome.quantity; + } else { + map.set(key, counter++); + outcomes.push(outcome); + } + }; + + merge.forEach(outcomes => outcomes.forEach(process)); + return outcomes; +} diff --git a/test/client.test.ts b/test/client.test.ts index cf3af434..682c083c 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,9 +1,16 @@ -import type { Envelope, Transport } from '@sentry/types'; +import type { Envelope, Outcome, Transport } from '@sentry/types'; import { CapacitorClient } from '../src/client'; import type { CapacitorClientOptions } from '../src/options'; import { NativeTransport } from '../src/transports/native'; import { NATIVE } from '../src/wrapper'; +import { + envelopeItemHeader, + envelopeItemPayload, + envelopeItems, + firstArg, + getSyncPromiseRejectOnFirstCall, +} from './testutils'; interface MockedCapacitor { Platform: { @@ -68,6 +75,8 @@ jest.mock('../src/plugin', () => { }; }); +import { rejectedSyncPromise,SentryError } from '@sentry/utils'; + import * as Plugin from '../src/plugin'; @@ -294,35 +303,35 @@ describe('Tests CapacitorClient', () => { }); }); */ - /* TODO: Fix SDKInfo - describe('event data enhancement', () => { - test('event contains sdk default information', async () => { - const mockedSend = jest.fn, [Envelope]>().mockResolvedValue(undefined); - const mockedTransport = (): Transport => ({ - send: mockedSend, - flush: jest.fn().mockResolvedValue(true), - }); - const client = new CapacitorClient({ - ...DEFAULT_OPTIONS, - dsn: EXAMPLE_DSN, - transport: mockedTransport, - }); - - client.captureEvent({ message: 'test event' }); - - expect(mockedSend).toBeCalled(); - const actualEvent: Event | undefined = ( - mockedSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload] - ); - expect(actualEvent?.sdk?.packages).toEqual([ - { - name: SDK_PACKAGE_NAME, - version: SDK_VERSION, - }, - ]); + /* TODO: Fix SDKInfo +describe('event data enhancement', () => { + test('event contains sdk default information', async () => { + const mockedSend = jest.fn, [Envelope]>().mockResolvedValue(undefined); + const mockedTransport = (): Transport => ({ + send: mockedSend, + flush: jest.fn().mockResolvedValue(true), + }); + const client = new CapacitorClient({ + ...DEFAULT_OPTIONS, + dsn: EXAMPLE_DSN, + transport: mockedTransport, }); + client.captureEvent({ message: 'test event' }); + + expect(mockedSend).toBeCalled(); + const actualEvent: Event | undefined = ( + mockedSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload] + ); + expect(actualEvent?.sdk?.packages).toEqual([ + { + name: SDK_PACKAGE_NAME, + version: SDK_VERSION, + }, + ]); }); + +}); */ describe('normalizes events', () => { /* TODO: Fix later @@ -359,7 +368,6 @@ describe('Tests CapacitorClient', () => { */ }); - /* TODO: To be fixed on Client Report implementation. describe('clientReports', () => { test('does not send client reports if disabled', () => { const mockTransportSend = jest.fn((_envelope: Envelope) => Promise.resolve()); @@ -380,7 +388,6 @@ describe('Tests CapacitorClient', () => { expectOnlyMessageEventInEnvelope(mockTransportSend); }); - /* TODO: Implement Client Report test('send client reports on event envelope', () => { const mockTransportSend = jest.fn((_envelope: Envelope) => Promise.resolve()); const client = new CapacitorClient({ @@ -431,9 +438,7 @@ describe('Tests CapacitorClient', () => { expectOnlyMessageEventInEnvelope(mockTransportSend); }); -*/ - /* - TODO: To be implemented + test('keeps outcomes in case envelope fails to send', () => { const mockTransportSend = jest.fn((_envelope: Envelope) => rejectedSyncPromise(new SentryError('Test'))); const client = new CapacitorClient({ @@ -454,10 +459,7 @@ describe('Tests CapacitorClient', () => { { reason: 'before_send', category: 'error', quantity: 1 }, ]); }); - */ - /* - TODO: To be implemented. test('sends buffered client reports on second try', () => { const mockTransportSend = getSyncPromiseRejectOnFirstCall<[Envelope]>(new SentryError('Test')); const client = new CapacitorClient({ @@ -493,9 +495,19 @@ describe('Tests CapacitorClient', () => { ); expect((client as unknown as { _outcomesBuffer: Outcome[] })._outcomesBuffer).toEqual([]); }); - */ - }); + function expectOnlyMessageEventInEnvelope(transportSend: jest.Mock) { + expect(transportSend).toBeCalledTimes(1); + expect(transportSend.mock.calls[0][firstArg][envelopeItems]).toHaveLength(1); + expect(transportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemHeader]).toEqual( + expect.objectContaining({ type: 'event' }), + ); + } + + function mockDroppedEvent(client: CapacitorClient) { + client.recordDroppedEvent('before_send', 'error'); + } + }); function mockedOptions(options: Partial): CapacitorClientOptions { return { @@ -508,3 +520,4 @@ describe('Tests CapacitorClient', () => { ...options, }; } +}); diff --git a/test/utils/outcomes.test.ts b/test/utils/outcomes.test.ts new file mode 100644 index 00000000..a5282f56 --- /dev/null +++ b/test/utils/outcomes.test.ts @@ -0,0 +1,129 @@ +import type { Outcome } from '@sentry/types'; + +import { mergeOutcomes } from '../../src/utils/outcome'; + +describe('mergeOutcomes', () => { + test('merge same outcomes into one incrementing the quantity', () => { + const outcome1: Outcome[] = + [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const outcome2: Outcome[] = + [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const expectedOutcome: Outcome[] = [{ + reason: 'before_send', + category: 'error', + quantity: 2, + }]; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); + + test('merge different outcomes into separated outcomes', () => { + const outcome1: Outcome[] = + [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const outcome2: Outcome[] = + [{ + reason: 'event_processor', + category: 'error', + quantity: 1, + }]; + const expectedOutcome: Outcome[] = [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }, + { + reason: 'event_processor', + category: 'error', + quantity: 1, + }]; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); + + test('merge outcomes when first outcome is empty', () => { + const outcome1: Outcome[] = []; + const outcome2: Outcome[] = + [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const expectedOutcome: Outcome[] = [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); + + test('merge outcomes when second outcome is empty', () => { + const outcome1: Outcome[] = + [{ + reason: 'event_processor', + category: 'error', + quantity: 1, + }]; + const expectedOutcome: Outcome[] = [{ + reason: 'event_processor', + category: 'error', + quantity: 1, + }]; + const outcome2: Outcome[] = []; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); + + test('empty outcomes return an array of empty outcomes', () => { + const outcome1: Outcome[] = []; + const expectedOutcome: Outcome[] = []; + const outcome2: Outcome[] = []; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); + + test('same outocmes but different category into separated outcomes', () => { + const outcome1: Outcome[] = + [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }]; + const outcome2: Outcome[] = + [{ + reason: 'before_send', + category: 'default', + quantity: 1, + }]; + const expectedOutcome: Outcome[] = [{ + reason: 'before_send', + category: 'error', + quantity: 1, + }, + { + reason: 'before_send', + category: 'default', + quantity: 1, + }]; + const finalOutcomes = mergeOutcomes(outcome1, outcome2); + + expect(finalOutcomes).toStrictEqual(expectedOutcome); + }); +});