From 2e5b0b81a745a3bfa501b28cc54e502b97405c44 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 25 Feb 2022 09:39:10 -0500 Subject: [PATCH] ref(browser): Introduce client reports envelope helper (#4588) 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. --- packages/browser/src/transports/base.ts | 34 +++++++--------- packages/types/src/clientreport.ts | 2 +- packages/utils/src/clientreport.ts | 24 +++++++++++ packages/utils/src/index.ts | 1 + packages/utils/test/clientreport.test.ts | 51 ++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 packages/utils/src/clientreport.ts create mode 100644 packages/utils/test/clientreport.test.ts 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}]}" + `); + }); +});