Skip to content

Commit

Permalink
feat(utils): Introduce envelope helper functions (#4587)
Browse files Browse the repository at this point in the history
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
#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 headers/items are typed accordingly.

To add items to envelopes, the`addItemToEnvelope()` functions is exposed. 
In the interest of keeping the API surface small, we settled with just this one
function, but we can come back and change it later on.

Finally, there is `serializeEnvelope()`, which is used to serialize an
envelope to a string. It does have some TypeScript complications, which
are 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.
  • Loading branch information
AbhiPrasad authored Feb 25, 2022
1 parent bb6f865 commit 041186e
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 7 deletions.
10 changes: 5 additions & 5 deletions packages/types/src/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ export type BaseEnvelopeItemHeaders = {
length?: number;
};

export type BaseEnvelopeItem<IH extends BaseEnvelopeItemHeaders, P extends unknown> = [IH, P]; // P is for payload
type BaseEnvelopeItem<IH extends BaseEnvelopeItemHeaders, P extends unknown> = [IH, P]; // P is for payload

export type BaseEnvelope<
EH extends BaseEnvelopeHeaders,
I extends BaseEnvelopeItem<BaseEnvelopeItemHeaders, unknown>,
> = [EH, I[]];
type BaseEnvelope<EH extends BaseEnvelopeHeaders, I extends BaseEnvelopeItem<BaseEnvelopeItemHeaders, unknown>> = [
EH,
I[],
];

type EventItemHeaders = { type: 'event' | 'transaction' };
type AttachmentItemHeaders = { type: 'attachment'; filename: string };
Expand Down
2 changes: 0 additions & 2 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export { DsnComponents, DsnLike, DsnProtocol } from './dsn';
export { DebugImage, DebugImageType, DebugMeta } from './debugMeta';
export {
AttachmentItem,
BaseEnvelope,
BaseEnvelopeHeaders,
BaseEnvelopeItem,
BaseEnvelopeItemHeaders,
ClientReportEnvelope,
ClientReportItem,
Expand Down
38 changes: 38 additions & 0 deletions packages/utils/src/envelope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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<E extends Envelope>(headers: E[0], items: E[1] = []): E {
return [headers, 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<E extends Envelope>(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);
}
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from './supports';
export * from './syncpromise';
export * from './time';
export * from './env';
export * from './envelope';
47 changes: 47 additions & 0 deletions packages/utils/test/envelope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { EventEnvelope } from '@sentry/types';

import { addItemToEnvelope, createEnvelope, serializeEnvelope } from '../src/envelope';
import { parseEnvelope } from './testutils';

describe('envelope', () => {
describe('createEnvelope()', () => {
const testTable: Array<[string, Parameters<typeof createEnvelope>[0], Parameters<typeof createEnvelope>[1]]> = [
['creates an empty envelope', {}, []],
['creates an envelope with a header but no items', { dsn: 'https://[email protected]/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('serializeEnvelope()', () => {
it('serializes an envelope', () => {
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
expect(serializeEnvelope(env)).toMatchInlineSnapshot(
`"{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"}"`,
);
});
});

describe('addItemToEnvelope()', () => {
it('adds an item to an envelope', () => {
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
const parsedEnvelope = parseEnvelope(serializeEnvelope(env));
expect(parsedEnvelope).toHaveLength(1);
expect(parsedEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });

const newEnv = addItemToEnvelope<EventEnvelope>(env, [
{ type: 'event' },
{ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' },
]);
const parsedNewEnvelope = parseEnvelope(serializeEnvelope(newEnv));
expect(parsedNewEnvelope).toHaveLength(3);
expect(parsedNewEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
expect(parsedNewEnvelope[1]).toEqual({ type: 'event' });
expect(parsedNewEnvelope[2]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' });
});
});
});
4 changes: 4 additions & 0 deletions packages/utils/test/testutils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {

return it;
};

export function parseEnvelope(env: string): Array<Record<any, any>> {
return env.split('\n').map(e => JSON.parse(e));
}

0 comments on commit 041186e

Please sign in to comment.