Skip to content

ref(browser): Introduce client reports envelope helper #4588

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Feb 25, 2022
Merged
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://[email protected]/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://[email protected]/1\\"}
{\\"type\\":\\"client_report\\"}
{\\"timestamp\\":123456,\\"discarded_events\\":[{\\"reason\\":\\"before_send\\",\\"category\\":\\"event\\",\\"quantity\\":30},{\\"reason\\":\\"network_error\\",\\"category\\":\\"transaction\\",\\"quantity\\":23}]}"
`);
});
});