diff --git a/packages/browser/src/transports/base.ts b/packages/browser/src/transports/base.ts index 6d7651a02423..e519dd56dc88 100644 --- a/packages/browser/src/transports/base.ts +++ b/packages/browser/src/transports/base.ts @@ -7,6 +7,7 @@ import { sessionToSentryRequest, } from '@sentry/core'; import { + ClientReport, Event, Outcome, Response as SentryResponse, @@ -17,7 +18,7 @@ import { TransportOptions, } from '@sentry/types'; import { - dateTimestampInSeconds, + createClientReportEnvelope, dsnToString, eventStatusFromHttpCode, getGlobalObject, @@ -26,6 +27,7 @@ import { makePromiseBuffer, parseRetryAfterHeader, PromiseBuffer, + serializeEnvelope, } from '@sentry/utils'; import { sendReport } from './utils'; @@ -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); } diff --git a/packages/types/src/clientreport.ts b/packages/types/src/clientreport.ts index add2194c13ff..22c590d0cc64 100644 --- a/packages/types/src/clientreport.ts +++ b/packages/types/src/clientreport.ts @@ -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 }>; }; diff --git a/packages/utils/src/clientreport.ts b/packages/utils/src/clientreport.ts new file mode 100644 index 000000000000..f91c79ab5c5c --- /dev/null +++ b/packages/utils/src/clientreport.ts @@ -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(dsn ? { dsn } : {}, [clientReportItem]); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 511d8a1315ca..6a8e84e2fd93 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -22,3 +22,4 @@ export * from './syncpromise'; export * from './time'; export * from './env'; export * from './envelope'; +export * from './clientreport'; diff --git a/packages/utils/test/clientreport.test.ts b/packages/utils/test/clientreport.test.ts new file mode 100644 index 000000000000..8d98291c3a14 --- /dev/null +++ b/packages/utils/test/clientreport.test.ts @@ -0,0 +1,51 @@ +import { ClientReport } from '@sentry/types'; + +import { createClientReportEnvelope } from '../src/clientreport'; +import { serializeEnvelope } from '../src/envelope'; + +const DEFAULT_DISCARDED_EVENTS: Array = [ + { + 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[0], Parameters[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}]}" + `); + }); +});