From 24240e96da514dd8cfac5582dd9b4590f38c3aea Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 15 Feb 2022 18:38:48 -0500 Subject: [PATCH] feat(utils): Introduce envelope helper functions This patch introduces functions that create, mutate and serialize envelopes. It also adds some basic unit tests that sanity check their functionality. It builds on top of the work done in https://github.com/getsentry/sentry-javascript/pull/4527. Users are expected to not directly interact with the Envelope instance, but instead use the helper functions to work with them. Essentially, we can treat the envelope instance as an opaque handle, similar to how void pointers are used in low-level languages. This was done to minimize the bundle impact of working with the envelopes, and as the set of possible envelope operations was fixed (and on the smaller end). To directly create an envelope, the `createEnvelope()` function was introduced. Users are encouraged to explicitly provide the generic type arg to this function so that add headers/items are typed accordingly. To add headers and items to envelopes, the `addHeaderToEnvelope()` and `addItemToEnvelope()` functions are exposed respectively. The reason that these functions are purely additive (or in the case of headers, can re-write existing ones), instead of allow for headers/items to be removed, is that removal functionality doesn't seem like it'll be used at all. In the interest of keeping the API surface small, we settled with these two functions, but we can come back and adjust this later on. Finally, there is `serializeEnvelope()`, which is used to serialize an envelope to a string. It does have some TypeScript complications, which is explained in detail in a code comment, but otherwise is a pretty simple implementation. You can notice the power of the tuple based envelope implementation, where it becomes easy to access headers/items. ```js const [headers, items] = envelope; ``` To illustrate how these functions will be used, another patch will be added that adds a `createClientReportEnvelope()` util, and the base transport in `@sentry/browser` will be updated to use that util. --- packages/utils/src/envelope.ts | 48 ++++++++++++++++++++++++++++ packages/utils/src/index.ts | 1 + packages/utils/test/envelope.test.ts | 42 ++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 packages/utils/src/envelope.ts create mode 100644 packages/utils/test/envelope.test.ts diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts new file mode 100644 index 000000000000..8ba572fa5779 --- /dev/null +++ b/packages/utils/src/envelope.ts @@ -0,0 +1,48 @@ +import { Envelope } from '@sentry/types'; + +/** + * Creates an envelope. + * Make sure to always explicitly provide the generic to this function + * so that the envelope types resolve correctly. + */ +export function createEnvelope(headers: E[0], items: E[1]): E { + return [headers, items] as E; +} + +/** + * Add a set of key value pairs to the envelope header. + * Make sure to always explicitly provide the generic to this function + * so that the envelope types resolve correctly. + */ +export function addHeaderToEnvelope(envelope: E, newHeaders: E[0]): E { + const [headers, items] = envelope; + return [{ ...headers, ...newHeaders }, items] as E; +} + +/** + * Add an item to an envelope. + * Make sure to always explicitly provide the generic to this function + * so that the envelope types resolve correctly. + */ +export function addItemToEnvelope(envelope: E, newItem: E[1][number]): E { + const [headers, items] = envelope; + return [headers, [...items, newItem]] as E; +} + +/** + * Serializes an envelope into a string. + */ +export function serializeEnvelope(envelope: Envelope): string { + const [headers, items] = envelope; + const serializedHeaders = JSON.stringify(headers); + + // Have to cast items to any here since Envelope is a union type + // Fixed in Typescript 4.2 + // TODO: Remove any[] cast when we upgrade to TS 4.2 + // https://github.com/microsoft/TypeScript/issues/36390 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (items as any[]).reduce((acc, item: typeof items[number]) => { + const [itemHeaders, payload] = item; + return `${acc}\n${JSON.stringify(itemHeaders)}\n${JSON.stringify(payload)}`; + }, serializedHeaders); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 900e50653037..511d8a1315ca 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -21,3 +21,4 @@ export * from './supports'; export * from './syncpromise'; export * from './time'; export * from './env'; +export * from './envelope'; diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts new file mode 100644 index 000000000000..451ca00ff1c8 --- /dev/null +++ b/packages/utils/test/envelope.test.ts @@ -0,0 +1,42 @@ +import { createEnvelope, addHeaderToEnvelope, addItemToEnvelope, serializeEnvelope } from '../src/envelope'; +import { EventEnvelope } from '@sentry/types'; + +describe('envelope', () => { + describe('createEnvelope()', () => { + const testTable: Array<[string, Parameters[0], Parameters[1]]> = [ + ['creates an empty envelope', {}, []], + ['creates an envelope with a header but no items', { dsn: 'https://public@example.com/1', sdk: {} }, []], + ]; + it.each(testTable)('%s', (_: string, headers, items) => { + const env = createEnvelope(headers, items); + expect(env).toHaveLength(2); + expect(env[0]).toStrictEqual(headers); + expect(env[1]).toStrictEqual(items); + }); + }); + + describe('addHeaderToEnvelope()', () => { + it('adds a header to the envelope', () => { + const env = createEnvelope({}, []); + expect(serializeEnvelope(env)).toMatchInlineSnapshot(`"{}"`); + const newEnv = addHeaderToEnvelope(env, { dsn: 'https://public@example.com/' }); + expect(serializeEnvelope(newEnv)).toMatchInlineSnapshot(`"{\\"dsn\\":\\"https://public@example.com/\\"}"`); + }); + }); + + describe('addItemToEnvelope()', () => { + const env = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []); + expect(serializeEnvelope(env)).toMatchInlineSnapshot( + `"{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"}"`, + ); + const newEnv = addItemToEnvelope(env, [ + { type: 'event' }, + { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }, + ]); + expect(serializeEnvelope(newEnv)).toMatchInlineSnapshot(` + "{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"} + {\\"type\\":\\"event\\"} + {\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\"}" + `); + }); +});