Skip to content

Commit

Permalink
ref(browser): Introduce client reports envelope helper (#4588)
Browse files Browse the repository at this point in the history
Leverage the new envelope utility functions to construct client report
envelopes sent in the browser transport. This also opens us up to more
easily add client reports to node or other environments.
  • Loading branch information
AbhiPrasad committed Feb 25, 2022
1 parent 041186e commit 2e5b0b8
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 20 deletions.
34 changes: 15 additions & 19 deletions packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
sessionToSentryRequest,
} from '@sentry/core';
import {
ClientReport,
Event,
Outcome,
Response as SentryResponse,
Expand All @@ -17,7 +18,7 @@ import {
TransportOptions,
} from '@sentry/types';
import {
dateTimestampInSeconds,
createClientReportEnvelope,
dsnToString,
eventStatusFromHttpCode,
getGlobalObject,
Expand All @@ -26,6 +27,7 @@ import {
makePromiseBuffer,
parseRetryAfterHeader,
PromiseBuffer,
serializeEnvelope,
} from '@sentry/utils';

import { sendReport } from './utils';
Expand Down Expand Up @@ -127,26 +129,20 @@ export abstract class BaseTransport implements Transport {
logger.log(`Flushing outcomes:\n${JSON.stringify(outcomes, null, 2)}`);

const url = getEnvelopeEndpointWithUrlEncodedAuth(this._api.dsn, this._api.tunnel);
// Envelope header is required to be at least an empty object
const envelopeHeader = JSON.stringify({ ...(this._api.tunnel && { dsn: dsnToString(this._api.dsn) }) });
const itemHeaders = JSON.stringify({
type: 'client_report',
});
const item = JSON.stringify({
timestamp: dateTimestampInSeconds(),
discarded_events: Object.keys(outcomes).map(key => {
const [category, reason] = key.split(':');
return {
reason,
category,
quantity: outcomes[key],
};
}),
});
const envelope = `${envelopeHeader}\n${itemHeaders}\n${item}`;

const discardedEvents = Object.keys(outcomes).map(key => {
const [category, reason] = key.split(':');
return {
reason,
category,
quantity: outcomes[key],
};
// TODO: Improve types on discarded_events to get rid of cast
}) as ClientReport['discarded_events'];
const envelope = createClientReportEnvelope(discardedEvents, this._api.tunnel && dsnToString(this._api.dsn));

try {
sendReport(url, envelope);
sendReport(url, serializeEnvelope(envelope));
} catch (e) {
logger.error(e);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/clientreport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { Outcome } from './transport';

export type ClientReport = {
timestamp: number;
discarded_events: { reason: Outcome; category: SentryRequestType; quantity: number };
discarded_events: Array<{ reason: Outcome; category: SentryRequestType; quantity: number }>;
};
24 changes: 24 additions & 0 deletions packages/utils/src/clientreport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ClientReport, ClientReportEnvelope, ClientReportItem } from '@sentry/types';

import { createEnvelope } from './envelope';
import { dateTimestampInSeconds } from './time';

/**
* Creates client report envelope
* @param discarded_events An array of discard events
* @param dsn A DSN that can be set on the header. Optional.
*/
export function createClientReportEnvelope(
discarded_events: ClientReport['discarded_events'],
dsn?: string,
timestamp?: number,
): ClientReportEnvelope {
const clientReportItem: ClientReportItem = [
{ type: 'client_report' },
{
timestamp: timestamp || dateTimestampInSeconds(),
discarded_events,
},
];
return createEnvelope<ClientReportEnvelope>(dsn ? { dsn } : {}, [clientReportItem]);
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './syncpromise';
export * from './time';
export * from './env';
export * from './envelope';
export * from './clientreport';
51 changes: 51 additions & 0 deletions packages/utils/test/clientreport.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { ClientReport } from '@sentry/types';

import { createClientReportEnvelope } from '../src/clientreport';
import { serializeEnvelope } from '../src/envelope';

const DEFAULT_DISCARDED_EVENTS: Array<ClientReport['discarded_events']> = [
{
reason: 'before_send',
category: 'event',
quantity: 30,
},
{
reason: 'network_error',
category: 'transaction',
quantity: 23,
},
];

const MOCK_DSN = 'https://public@example.com/1';

describe('createClientReportEnvelope', () => {
const testTable: Array<
[string, Parameters<typeof createClientReportEnvelope>[0], Parameters<typeof createClientReportEnvelope>[1]]
> = [
['with no discard reasons', [], undefined],
['with a dsn', [], MOCK_DSN],
['with discard reasons', DEFAULT_DISCARDED_EVENTS, MOCK_DSN],
];
it.each(testTable)('%s', (_: string, discardedEvents, dsn) => {
const env = createClientReportEnvelope(discardedEvents, dsn);

expect(env[0]).toEqual(dsn ? { dsn } : {});

const items = env[1];
expect(items).toHaveLength(1);
const clientReportItem = items[0];

expect(clientReportItem[0]).toEqual({ type: 'client_report' });
expect(clientReportItem[1]).toEqual({ timestamp: expect.any(Number), discarded_events: discardedEvents });
});

it('serializes an envelope', () => {
const env = createClientReportEnvelope(DEFAULT_DISCARDED_EVENTS, MOCK_DSN, 123456);
const serializedEnv = serializeEnvelope(env);
expect(serializedEnv).toMatchInlineSnapshot(`
"{\\"dsn\\":\\"https://public@example.com/1\\"}
{\\"type\\":\\"client_report\\"}
{\\"timestamp\\":123456,\\"discarded_events\\":[{\\"reason\\":\\"before_send\\",\\"category\\":\\"event\\",\\"quantity\\":30},{\\"reason\\":\\"network_error\\",\\"category\\":\\"transaction\\",\\"quantity\\":23}]}"
`);
});
});

0 comments on commit 2e5b0b8

Please sign in to comment.