Skip to content

Commit f0d21c2

Browse files
author
Luca Forstner
committed
ref: Record dropped events in BaseClient
1 parent 152a8ec commit f0d21c2

File tree

8 files changed

+184
-96
lines changed

8 files changed

+184
-96
lines changed

packages/core/src/baseclient.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { Scope, Session } from '@sentry/hub';
33
import {
44
Client,
55
ClientOptions,
6+
DataCategory,
67
DsnComponents,
78
Envelope,
89
Event,
10+
EventDropReason,
911
EventHint,
1012
Integration,
1113
IntegrationClass,
14+
Outcome,
1215
SessionAggregates,
1316
Severity,
1417
SeverityLevel,
@@ -87,6 +90,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
8790
/** Number of calls being processed */
8891
protected _numProcessing: number = 0;
8992

93+
/** Holds flushable */
94+
private _outcomes: { [key: string]: number } = {};
95+
9096
/**
9197
* Initializes this client instance.
9298
*
@@ -98,7 +104,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
98104
this._dsn = makeDsn(options.dsn);
99105
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options.tunnel);
100106
this._transport = options.transport({
101-
recordDroppedEvent: () => undefined, // TODO(v7): Provide a proper function instead of noop
107+
recordDroppedEvent: this.recordDroppedEvent.bind(this),
102108
...options.transportOptions,
103109
url,
104110
});
@@ -284,6 +290,23 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
284290
}
285291
}
286292

293+
/**
294+
* @inheritDoc
295+
*/
296+
public recordDroppedEvent(reason: EventDropReason, category: DataCategory): void {
297+
// We want to track each category (error, transaction, session) separately
298+
// but still keep the distinction between different type of outcomes.
299+
// We could use nested maps, but it's much easier to read and type this way.
300+
// A correct type for map-based implementation if we want to go that route
301+
// would be `Partial<Record<SentryRequestType, Partial<Record<Outcome, number>>>>`
302+
// With typescript 4.1 we could even use template literal types
303+
const key = `${reason}:${category}`;
304+
IS_DEBUG_BUILD && logger.log(`Adding outcome: "${key}"`);
305+
306+
// The following works because undefined + 1 === NaN and NaN is falsy
307+
this._outcomes[key] = this._outcomes[key] + 1 || 1;
308+
}
309+
287310
/** Updates existing session based on the provided event */
288311
protected _updateSessionFromEvent(session: Session, event: Event): void {
289312
let crashed = false;
@@ -552,20 +575,8 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
552575
* @returns A SyncPromise that resolves with the event or rejects in case event was/will not be send.
553576
*/
554577
protected _processEvent(event: Event, hint?: EventHint, scope?: Scope): PromiseLike<Event> {
555-
// eslint-disable-next-line @typescript-eslint/unbound-method
556578
const { beforeSend, sampleRate } = this.getOptions();
557579

558-
// type RecordLostEvent = NonNullable<Transport['recordLostEvent']>;
559-
type RecordLostEventParams = unknown[]; // Parameters<RecordLostEvent>;
560-
561-
// no-op as new transports don't have client outcomes
562-
// TODO(v7): Re-add this functionality
563-
function recordLostEvent(_outcome: RecordLostEventParams[0], _category: RecordLostEventParams[1]): void {
564-
// if (transport.recordLostEvent) {
565-
// transport.recordLostEvent(outcome, category);
566-
// }
567-
}
568-
569580
if (!this._isEnabled()) {
570581
return rejectedSyncPromise(new SentryError('SDK not enabled, will not capture event.'));
571582
}
@@ -575,7 +586,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
575586
// 0.0 === 0% events are sent
576587
// Sampling for transaction happens somewhere else
577588
if (!isTransaction && typeof sampleRate === 'number' && Math.random() > sampleRate) {
578-
recordLostEvent('sample_rate', 'event');
589+
this.recordDroppedEvent('sample_rate', 'error');
579590
return rejectedSyncPromise(
580591
new SentryError(
581592
`Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`,
@@ -586,7 +597,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
586597
return this._prepareEvent(event, scope, hint)
587598
.then(prepared => {
588599
if (prepared === null) {
589-
recordLostEvent('event_processor', event.type || 'event');
600+
this.recordDroppedEvent('event_processor', event.type || 'error');
590601
throw new SentryError('An event processor returned null, will not send event.');
591602
}
592603

@@ -600,7 +611,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
600611
})
601612
.then(processedEvent => {
602613
if (processedEvent === null) {
603-
recordLostEvent('before_send', event.type || 'event');
614+
this.recordDroppedEvent('before_send', event.type || 'error');
604615
throw new SentryError('`beforeSend` returned `null`, will not send event.');
605616
}
606617

@@ -649,7 +660,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
649660
/**
650661
* @inheritdoc
651662
*/
652-
private _sendEnvelope(envelope: Envelope): void {
663+
protected _sendEnvelope(envelope: Envelope): void {
653664
if (this._transport && this._dsn) {
654665
this._transport.send(envelope).then(null, reason => {
655666
IS_DEBUG_BUILD && logger.error('Error while sending event:', reason);
@@ -659,6 +670,22 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
659670
}
660671
}
661672

673+
/**
674+
* Clears outcomes on this client and returns them.
675+
*/
676+
protected _clearOutcomes(): Outcome[] {
677+
const outcomes = this._outcomes;
678+
this._outcomes = {};
679+
return Object.keys(outcomes).map(key => {
680+
const [reason, category] = key.split(':') as [EventDropReason, DataCategory];
681+
return {
682+
reason,
683+
category,
684+
quantity: outcomes[key],
685+
};
686+
});
687+
}
688+
662689
/**
663690
* @inheritDoc
664691
*/

packages/core/test/lib/base.test.ts

Lines changed: 101 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,30 +1061,24 @@ describe('BaseClient', () => {
10611061
expect((TestClient.instance!.event! as any).data).toBe('someRandomThing');
10621062
});
10631063

1064-
// TODO(v7): Add back test with client reports
1065-
// test('beforeSend records dropped events', () => {
1066-
// expect.assertions(1);
1067-
1068-
// const client = new TestClient(
1069-
// getDefaultTestClientOptions({
1070-
// dsn: PUBLIC_DSN,
1071-
// beforeSend() {
1072-
// return null;
1073-
// },
1074-
// }),
1075-
// );
1076-
// const recordLostEventSpy = jest.fn();
1077-
// jest.spyOn(client, 'getTransport').mockImplementationOnce(
1078-
// () =>
1079-
// ({
1080-
// recordLostEvent: recordLostEventSpy,
1081-
// } as any as Transport),
1082-
// );
1083-
1084-
// client.captureEvent({ message: 'hello' }, {});
1085-
1086-
// expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'event');
1087-
// });
1064+
test('beforeSend records dropped events', () => {
1065+
expect.assertions(1);
1066+
1067+
const client = new TestClient(
1068+
getDefaultTestClientOptions({
1069+
dsn: PUBLIC_DSN,
1070+
beforeSend() {
1071+
return null;
1072+
},
1073+
}),
1074+
);
1075+
1076+
const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent');
1077+
1078+
client.captureEvent({ message: 'hello' }, {});
1079+
1080+
expect(recordLostEventSpy).toHaveBeenCalledWith('before_send', 'error');
1081+
});
10881082

10891083
test('eventProcessor can drop the even when it returns null', () => {
10901084
expect.assertions(3);
@@ -1102,28 +1096,21 @@ describe('BaseClient', () => {
11021096
expect(loggerErrorSpy).toBeCalledWith(new SentryError('An event processor returned null, will not send event.'));
11031097
});
11041098

1105-
// TODO(v7): Add back tests with client reports
1106-
// test('eventProcessor records dropped events', () => {
1107-
// expect.assertions(1);
1099+
test('eventProcessor records dropped events', () => {
1100+
expect.assertions(1);
11081101

1109-
// const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1110-
// const client = new TestClient(options);
1102+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1103+
const client = new TestClient(options);
11111104

1112-
// const recordLostEventSpy = jest.fn();
1113-
// jest.spyOn(client, 'getTransport').mockImplementationOnce(
1114-
// () =>
1115-
// ({
1116-
// recordLostEvent: recordLostEventSpy,
1117-
// } as any as Transport),
1118-
// );
1105+
const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent');
11191106

1120-
// const scope = new Scope();
1121-
// scope.addEventProcessor(() => null);
1107+
const scope = new Scope();
1108+
scope.addEventProcessor(() => null);
11221109

1123-
// client.captureEvent({ message: 'hello' }, {}, scope);
1110+
client.captureEvent({ message: 'hello' }, {}, scope);
11241111

1125-
// expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'event');
1126-
// });
1112+
expect(recordLostEventSpy).toHaveBeenCalledWith('event_processor', 'error');
1113+
});
11271114

11281115
test('eventProcessor sends an event and logs when it crashes', () => {
11291116
expect.assertions(3);
@@ -1154,24 +1141,17 @@ describe('BaseClient', () => {
11541141
);
11551142
});
11561143

1157-
// TODO(v7): Add back test with client reports
1158-
// test('records events dropped due to sampleRate', () => {
1159-
// expect.assertions(1);
1144+
test('records events dropped due to sampleRate', () => {
1145+
expect.assertions(1);
11601146

1161-
// const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, sampleRate: 0 });
1162-
// const client = new TestClient(options);
1147+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, sampleRate: 0 });
1148+
const client = new TestClient(options);
11631149

1164-
// const recordLostEventSpy = jest.fn();
1165-
// jest.spyOn(client, 'getTransport').mockImplementationOnce(
1166-
// () =>
1167-
// ({
1168-
// recordLostEvent: recordLostEventSpy,
1169-
// } as any as Transport),
1170-
// );
1150+
const recordLostEventSpy = jest.spyOn(client, 'recordDroppedEvent');
11711151

1172-
// client.captureEvent({ message: 'hello' }, {});
1173-
// expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'event');
1174-
// });
1152+
client.captureEvent({ message: 'hello' }, {});
1153+
expect(recordLostEventSpy).toHaveBeenCalledWith('sample_rate', 'error');
1154+
});
11751155
});
11761156

11771157
describe('integrations', () => {
@@ -1366,4 +1346,69 @@ describe('BaseClient', () => {
13661346
expect(TestClient.instance!.session).toBeUndefined();
13671347
});
13681348
});
1349+
1350+
describe('recordDroppedEvent()/_clearOutcomes()', () => {
1351+
test('record and return outcomes', () => {
1352+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1353+
const client = new TestClient(options);
1354+
1355+
client.recordDroppedEvent('ratelimit_backoff', 'error');
1356+
client.recordDroppedEvent('ratelimit_backoff', 'error');
1357+
client.recordDroppedEvent('network_error', 'transaction');
1358+
client.recordDroppedEvent('network_error', 'transaction');
1359+
client.recordDroppedEvent('before_send', 'session');
1360+
client.recordDroppedEvent('event_processor', 'attachment');
1361+
client.recordDroppedEvent('network_error', 'transaction');
1362+
1363+
const clearedOutcomes = client._clearOutcomes();
1364+
1365+
expect(clearedOutcomes).toEqual(
1366+
expect.arrayContaining([
1367+
{
1368+
reason: 'ratelimit_backoff',
1369+
category: 'error',
1370+
quantity: 2,
1371+
},
1372+
{
1373+
reason: 'network_error',
1374+
category: 'transaction',
1375+
quantity: 3,
1376+
},
1377+
{
1378+
reason: 'before_send',
1379+
category: 'session',
1380+
quantity: 1,
1381+
},
1382+
{
1383+
reason: 'event_processor',
1384+
category: 'attachment',
1385+
quantity: 1,
1386+
},
1387+
]),
1388+
);
1389+
});
1390+
1391+
test('to clear outcomes', () => {
1392+
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
1393+
const client = new TestClient(options);
1394+
1395+
client.recordDroppedEvent('ratelimit_backoff', 'error');
1396+
client.recordDroppedEvent('ratelimit_backoff', 'error');
1397+
client.recordDroppedEvent('event_processor', 'attachment');
1398+
1399+
const clearedOutcomes1 = client._clearOutcomes();
1400+
expect(clearedOutcomes1.length).toEqual(2);
1401+
1402+
const clearedOutcomes2 = client._clearOutcomes();
1403+
expect(clearedOutcomes2.length).toEqual(0);
1404+
1405+
client.recordDroppedEvent('network_error', 'attachment');
1406+
1407+
const clearedOutcomes3 = client._clearOutcomes();
1408+
expect(clearedOutcomes3.length).toEqual(1);
1409+
1410+
const clearedOutcomes4 = client._clearOutcomes();
1411+
expect(clearedOutcomes4.length).toEqual(0);
1412+
});
1413+
});
13691414
});

packages/core/test/mocks/client.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
import { Session } from '@sentry/hub';
2-
import { ClientOptions, Event, Integration, Severity, SeverityLevel } from '@sentry/types';
2+
import { ClientOptions, Event, Integration, Outcome, Severity, SeverityLevel } from '@sentry/types';
33
import { resolvedSyncPromise } from '@sentry/utils';
44

55
import { BaseClient } from '../../src/baseclient';
66
import { initAndBind } from '../../src/sdk';
77
import { createTransport } from '../../src/transports/base';
88

9-
// TODO(v7): Add client reports tests to this file
10-
// See old tests in packages/browser/test/unit/transports/base.test.ts
11-
// from https://github.com/getsentry/sentry-javascript/pull/4967
12-
139
export function getDefaultTestClientOptions(options: Partial<TestClientOptions> = {}): TestClientOptions {
1410
return {
1511
integrations: [],
@@ -79,6 +75,11 @@ export class TestClient extends BaseClient<TestClientOptions> {
7975
public sendSession(session: Session): void {
8076
this.session = session;
8177
}
78+
79+
// Public proxy for protected method
80+
public _clearOutcomes(): Outcome[] {
81+
return super._clearOutcomes();
82+
}
8283
}
8384

8485
export function init(options: TestClientOptions): void {

packages/tracing/src/transaction.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,11 @@ export class Transaction extends SpanClass implements TransactionInterface {
103103
// At this point if `sampled !== true` we want to discard the transaction.
104104
IS_DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.');
105105

106-
// TODO(v7): Add back client reports functionality
107-
// const client = this._hub.getClient();
108-
// const transport = client && client.getTransport && client.getTransport();
109-
// if (transport && transport.recordLostEvent) {
110-
// transport.recordLostEvent('sample_rate', 'transaction');
111-
// }
106+
const client = this._hub.getClient();
107+
if (client) {
108+
client.recordDroppedEvent('sample_rate', 'transaction');
109+
}
110+
112111
return undefined;
113112
}
114113

0 commit comments

Comments
 (0)