Skip to content

Commit fc6613d

Browse files
committed
ref(browser): Introduce client reports envelope helper
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.
1 parent 4e4ae65 commit fc6613d

File tree

6 files changed

+102
-21
lines changed

6 files changed

+102
-21
lines changed

packages/browser/src/transports/base.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
sessionToSentryRequest,
88
} from '@sentry/core';
99
import {
10+
ClientReportEnvelopeItemPayload,
1011
Event,
1112
Outcome,
1213
Response as SentryResponse,
@@ -17,7 +18,7 @@ import {
1718
TransportOptions,
1819
} from '@sentry/types';
1920
import {
20-
dateTimestampInSeconds,
21+
createClientReportEnvelope,
2122
dsnToString,
2223
eventStatusFromHttpCode,
2324
getGlobalObject,
@@ -26,6 +27,7 @@ import {
2627
makePromiseBuffer,
2728
parseRetryAfterHeader,
2829
PromiseBuffer,
30+
serializeEnvelope,
2931
} from '@sentry/utils';
3032

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

129131
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._api.dsn, this._api.tunnel);
130-
// Envelope header is required to be at least an empty object
131-
const envelopeHeader = JSON.stringify({ ...(this._api.tunnel && { dsn: dsnToString(this._api.dsn) }) });
132-
const itemHeaders = JSON.stringify({
133-
type: 'client_report',
134-
});
135-
const item = JSON.stringify({
136-
timestamp: dateTimestampInSeconds(),
137-
discarded_events: Object.keys(outcomes).map(key => {
138-
const [category, reason] = key.split(':');
139-
return {
140-
reason,
141-
category,
142-
quantity: outcomes[key],
143-
};
144-
}),
145-
});
146-
const envelope = `${envelopeHeader}\n${itemHeaders}\n${item}`;
132+
133+
const discardedEvents = Object.keys(outcomes).map(key => {
134+
const [category, reason] = key.split(':');
135+
return {
136+
reason,
137+
category,
138+
quantity: outcomes[key],
139+
};
140+
// TODO: Improve types on discarded_events to get rid of cast
141+
}) as ClientReportEnvelopeItemPayload['discarded_events'];
142+
const envelope = createClientReportEnvelope(discardedEvents, this._api.tunnel && dsnToString(this._api.dsn));
147143

148144
try {
149-
sendReport(url, envelope);
145+
sendReport(url, serializeEnvelope(envelope));
150146
} catch (e) {
151147
logger.error(e);
152148
}

packages/types/src/envelope.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,15 @@ export type SessionEnvelopeItem =
5959

6060
export type SessionEnvelope = BaseEnvelope<{ sent_at: string }, SessionEnvelopeItem>;
6161

62+
export type ClientReportEnvelopeItemHeader = { type: 'client_report' };
63+
export type ClientReportEnvelopeItemPayload = {
64+
timestamp: number;
65+
discarded_events: Array<{ reason: Outcome; category: SentryRequestType; quantity: number }>;
66+
};
67+
6268
export type ClientReportEnvelopeItem = BaseEnvelopeItem<
63-
{ type: 'client_report' },
64-
{ timestamp: number; discarded_events: { reason: Outcome; category: SentryRequestType; quantity: number } }
69+
ClientReportEnvelopeItemHeader,
70+
ClientReportEnvelopeItemPayload
6571
>;
6672

6773
export type ClientReportEnvelope = BaseEnvelope<Record<string, unknown>, ClientReportEnvelopeItem>;

packages/types/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ export { Context, Contexts } from './context';
44
export { DsnComponents, DsnLike, DsnProtocol } from './dsn';
55
export { DebugImage, DebugImageType, DebugMeta } from './debugMeta';
66
export {
7+
BaseEnvelope,
78
ClientReportEnvelope,
89
ClientReportEnvelopeItem,
10+
ClientReportEnvelopeItemHeader,
11+
ClientReportEnvelopeItemPayload,
912
Envelope,
1013
EventEnvelope,
1114
EventEnvelopeItem,

packages/utils/src/clientreports.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ClientReportEnvelope, ClientReportEnvelopeItemHeader, ClientReportEnvelopeItemPayload } from '@sentry/types';
2+
3+
import { createEnvelope } from './envelope';
4+
import { dateTimestampInSeconds } from './time';
5+
6+
/**
7+
* Creates client report envelope
8+
* @param discarded_events An array of discard events
9+
* @param dsn A DSN that can be set on the header. Optional.
10+
*/
11+
export function createClientReportEnvelope(
12+
discarded_events: ClientReportEnvelopeItemPayload['discarded_events'],
13+
dsn?: string,
14+
timestamp?: number,
15+
): ClientReportEnvelope {
16+
const header = dsn ? { dsn } : {};
17+
18+
const itemHeader: ClientReportEnvelopeItemHeader = { type: 'client_report' };
19+
const itemPayload: ClientReportEnvelopeItemPayload = {
20+
timestamp: timestamp || dateTimestampInSeconds(),
21+
discarded_events,
22+
};
23+
24+
return createEnvelope<ClientReportEnvelope>(header, [[itemHeader, itemPayload]]);
25+
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export * from './syncpromise';
2222
export * from './time';
2323
export * from './env';
2424
export * from './envelope';
25+
export * from './clientreports';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createClientReportEnvelope } from '../src/clientreports';
2+
import { serializeEnvelope } from '../src/envelope';
3+
import { ClientReportEnvelopeItemPayload } from '@sentry/types';
4+
5+
const DEFAULT_DISCARDED_EVENTS: ClientReportEnvelopeItemPayload['discarded_events'] = [
6+
{
7+
reason: 'before_send',
8+
category: 'event',
9+
quantity: 30,
10+
},
11+
{
12+
reason: 'network_error',
13+
category: 'transaction',
14+
quantity: 23,
15+
},
16+
];
17+
18+
const MOCK_DSN = 'https://[email protected]/1';
19+
20+
describe('createClientReportEnvelope', () => {
21+
const testTable: Array<
22+
[string, Parameters<typeof createClientReportEnvelope>[0], Parameters<typeof createClientReportEnvelope>[1]]
23+
> = [
24+
['with no discard reasons', [], undefined],
25+
['with a dsn', [], MOCK_DSN],
26+
['with discard reasons', DEFAULT_DISCARDED_EVENTS, MOCK_DSN],
27+
];
28+
it.each(testTable)('%s', (_: string, discardedEvents, dsn) => {
29+
const env = createClientReportEnvelope(discardedEvents, dsn);
30+
31+
expect(env[0]).toEqual(dsn ? { dsn } : {});
32+
33+
const items = env[1];
34+
expect(items).toHaveLength(1);
35+
const clientReportItem = items[0];
36+
37+
expect(clientReportItem[0]).toEqual({ type: 'client_report' });
38+
expect(clientReportItem[1]).toEqual({ timestamp: expect.any(Number), discarded_events: discardedEvents });
39+
});
40+
41+
it('serializes an envelope', () => {
42+
const env = createClientReportEnvelope(DEFAULT_DISCARDED_EVENTS, MOCK_DSN, 123456);
43+
const serializedEnv = serializeEnvelope(env);
44+
expect(serializedEnv).toMatchInlineSnapshot(`
45+
"{\\"dsn\\":\\"https://[email protected]/1\\"}
46+
{\\"type\\":\\"client_report\\"}
47+
{\\"timestamp\\":123456,\\"discarded_events\\":[{\\"reason\\":\\"before_send\\",\\"category\\":\\"event\\",\\"quantity\\":30},{\\"reason\\":\\"network_error\\",\\"category\\":\\"transaction\\",\\"quantity\\":23}]}"
48+
`);
49+
});
50+
});

0 commit comments

Comments
 (0)