Skip to content

Commit

Permalink
add client report support
Browse files Browse the repository at this point in the history
  • Loading branch information
lucas-zimerman committed Jan 24, 2024
1 parent 7774b08 commit 6d44bc8
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 48 deletions.
34 changes: 23 additions & 11 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { eventFromException, eventFromMessage } from '@sentry/browser';
import { BaseClient } from '@sentry/core';
import type {
ClientReportEnvelope,
ClientReportItem,
Envelope,
Event,
EventHint,
Expand All @@ -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.
*
Expand Down Expand Up @@ -107,8 +112,7 @@ export class CapacitorClient extends BaseClient<CapacitorClientOptions> {
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;
Expand Down Expand Up @@ -162,12 +166,20 @@ export class CapacitorClient extends BaseClient<CapacitorClientOptions> {
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);
}
}
}

24 changes: 24 additions & 0 deletions src/utils/outcome.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>();
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;
}
87 changes: 50 additions & 37 deletions test/client.test.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down Expand Up @@ -68,6 +75,8 @@ jest.mock('../src/plugin', () => {
};
});

import { rejectedSyncPromise,SentryError } from '@sentry/utils';

import * as Plugin from '../src/plugin';


Expand Down Expand Up @@ -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<PromiseLike<void>, [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 = <Event>(
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<PromiseLike<void>, [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 = <Event>(
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
Expand Down Expand Up @@ -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());
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -493,9 +495,19 @@ describe('Tests CapacitorClient', () => {
);
expect((client as unknown as { _outcomesBuffer: Outcome[] })._outcomesBuffer).toEqual(<Outcome[]>[]);
});
*/
});

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>): CapacitorClientOptions {
return {
Expand All @@ -508,3 +520,4 @@ describe('Tests CapacitorClient', () => {
...options,
};
}
});
129 changes: 129 additions & 0 deletions test/utils/outcomes.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 6d44bc8

Please sign in to comment.