Skip to content

Commit 041186e

Browse files
authored
feat(utils): Introduce envelope helper functions (#4587)
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.
1 parent bb6f865 commit 041186e

File tree

6 files changed

+95
-7
lines changed

6 files changed

+95
-7
lines changed

packages/types/src/envelope.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export type BaseEnvelopeItemHeaders = {
1818
length?: number;
1919
};
2020

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

23-
export type BaseEnvelope<
24-
EH extends BaseEnvelopeHeaders,
25-
I extends BaseEnvelopeItem<BaseEnvelopeItemHeaders, unknown>,
26-
> = [EH, I[]];
23+
type BaseEnvelope<EH extends BaseEnvelopeHeaders, I extends BaseEnvelopeItem<BaseEnvelopeItemHeaders, unknown>> = [
24+
EH,
25+
I[],
26+
];
2727

2828
type EventItemHeaders = { type: 'event' | 'transaction' };
2929
type AttachmentItemHeaders = { type: 'attachment'; filename: string };

packages/types/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ export { DsnComponents, DsnLike, DsnProtocol } from './dsn';
66
export { DebugImage, DebugImageType, DebugMeta } from './debugMeta';
77
export {
88
AttachmentItem,
9-
BaseEnvelope,
109
BaseEnvelopeHeaders,
11-
BaseEnvelopeItem,
1210
BaseEnvelopeItemHeaders,
1311
ClientReportEnvelope,
1412
ClientReportItem,

packages/utils/src/envelope.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Envelope } from '@sentry/types';
2+
3+
/**
4+
* Creates an envelope.
5+
* Make sure to always explicitly provide the generic to this function
6+
* so that the envelope types resolve correctly.
7+
*/
8+
export function createEnvelope<E extends Envelope>(headers: E[0], items: E[1] = []): E {
9+
return [headers, items] as E;
10+
}
11+
12+
/**
13+
* Add an item to an envelope.
14+
* Make sure to always explicitly provide the generic to this function
15+
* so that the envelope types resolve correctly.
16+
*/
17+
export function addItemToEnvelope<E extends Envelope>(envelope: E, newItem: E[1][number]): E {
18+
const [headers, items] = envelope;
19+
return [headers, [...items, newItem]] as E;
20+
}
21+
22+
/**
23+
* Serializes an envelope into a string.
24+
*/
25+
export function serializeEnvelope(envelope: Envelope): string {
26+
const [headers, items] = envelope;
27+
const serializedHeaders = JSON.stringify(headers);
28+
29+
// Have to cast items to any here since Envelope is a union type
30+
// Fixed in Typescript 4.2
31+
// TODO: Remove any[] cast when we upgrade to TS 4.2
32+
// https://github.com/microsoft/TypeScript/issues/36390
33+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
34+
return (items as any[]).reduce((acc, item: typeof items[number]) => {
35+
const [itemHeaders, payload] = item;
36+
return `${acc}\n${JSON.stringify(itemHeaders)}\n${JSON.stringify(payload)}`;
37+
}, serializedHeaders);
38+
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export * from './supports';
2121
export * from './syncpromise';
2222
export * from './time';
2323
export * from './env';
24+
export * from './envelope';

packages/utils/test/envelope.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { EventEnvelope } from '@sentry/types';
2+
3+
import { addItemToEnvelope, createEnvelope, serializeEnvelope } from '../src/envelope';
4+
import { parseEnvelope } from './testutils';
5+
6+
describe('envelope', () => {
7+
describe('createEnvelope()', () => {
8+
const testTable: Array<[string, Parameters<typeof createEnvelope>[0], Parameters<typeof createEnvelope>[1]]> = [
9+
['creates an empty envelope', {}, []],
10+
['creates an envelope with a header but no items', { dsn: 'https://[email protected]/1', sdk: {} }, []],
11+
];
12+
it.each(testTable)('%s', (_: string, headers, items) => {
13+
const env = createEnvelope(headers, items);
14+
expect(env).toHaveLength(2);
15+
expect(env[0]).toStrictEqual(headers);
16+
expect(env[1]).toStrictEqual(items);
17+
});
18+
});
19+
20+
describe('serializeEnvelope()', () => {
21+
it('serializes an envelope', () => {
22+
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
23+
expect(serializeEnvelope(env)).toMatchInlineSnapshot(
24+
`"{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"}"`,
25+
);
26+
});
27+
});
28+
29+
describe('addItemToEnvelope()', () => {
30+
it('adds an item to an envelope', () => {
31+
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
32+
const parsedEnvelope = parseEnvelope(serializeEnvelope(env));
33+
expect(parsedEnvelope).toHaveLength(1);
34+
expect(parsedEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
35+
36+
const newEnv = addItemToEnvelope<EventEnvelope>(env, [
37+
{ type: 'event' },
38+
{ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' },
39+
]);
40+
const parsedNewEnvelope = parseEnvelope(serializeEnvelope(newEnv));
41+
expect(parsedNewEnvelope).toHaveLength(3);
42+
expect(parsedNewEnvelope[0]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
43+
expect(parsedNewEnvelope[1]).toEqual({ type: 'event' });
44+
expect(parsedNewEnvelope[2]).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' });
45+
});
46+
});
47+
});

packages/utils/test/testutils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
1111

1212
return it;
1313
};
14+
15+
export function parseEnvelope(env: string): Array<Record<any, any>> {
16+
return env.split('\n').map(e => JSON.parse(e));
17+
}

0 commit comments

Comments
 (0)