Skip to content

Commit 604cbf1

Browse files
authored
feat(utils): Improved envelope parser (#6580)
1 parent 6a275c0 commit 604cbf1

File tree

5 files changed

+76
-75
lines changed

5 files changed

+76
-75
lines changed

packages/core/test/lib/attachments.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { parseEnvelope } from '@sentry/utils/test/testutils';
2-
import { TextEncoder } from 'util';
1+
import { parseEnvelope } from '@sentry/utils';
2+
import { TextDecoder, TextEncoder } from 'util';
33

44
import { createTransport } from '../../src/transports/base';
55
import { getDefaultTestClientOptions, TestClient } from '../mocks/client';
@@ -22,7 +22,7 @@ describe('Attachments', () => {
2222
enableSend: true,
2323
transport: () =>
2424
createTransport({ recordDroppedEvent: () => undefined, textEncoder: new TextEncoder() }, async req => {
25-
const [, items] = parseEnvelope(req.body);
25+
const [, items] = parseEnvelope(req.body, new TextEncoder(), new TextDecoder());
2626
expect(items.length).toEqual(2);
2727
// Second envelope item should be the attachment
2828
expect(items[1][0]).toEqual({ type: 'attachment', length: 50000, filename: 'empty.bin' });

packages/utils/src/envelope.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
Attachment,
33
AttachmentItem,
4+
BaseEnvelopeHeaders,
5+
BaseEnvelopeItemHeaders,
46
DataCategory,
57
DsnComponents,
68
Envelope,
@@ -110,6 +112,51 @@ function concatBuffers(buffers: Uint8Array[]): Uint8Array {
110112
return merged;
111113
}
112114

115+
interface TextDecoderInternal {
116+
decode(input?: Uint8Array): string;
117+
}
118+
119+
/**
120+
* Parses an envelope
121+
*/
122+
export function parseEnvelope(
123+
env: string | Uint8Array,
124+
textEncoder: TextEncoderInternal,
125+
textDecoder: TextDecoderInternal,
126+
): Envelope {
127+
let buffer = typeof env === 'string' ? textEncoder.encode(env) : env;
128+
129+
function readBinary(length: number): Uint8Array {
130+
const bin = buffer.subarray(0, length);
131+
// Replace the buffer with the remaining data excluding trailing newline
132+
buffer = buffer.subarray(length + 1);
133+
return bin;
134+
}
135+
136+
function readJson<T>(): T {
137+
let i = buffer.indexOf(0xa);
138+
// If we couldn't find a newline, we must have found the end of the buffer
139+
if (i < 0) {
140+
i = buffer.length;
141+
}
142+
143+
return JSON.parse(textDecoder.decode(readBinary(i))) as T;
144+
}
145+
146+
const envelopeHeader = readJson<BaseEnvelopeHeaders>();
147+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
148+
const items: [any, any][] = [];
149+
150+
while (buffer.length) {
151+
const itemHeader = readJson<BaseEnvelopeItemHeaders>();
152+
const binaryLength = typeof itemHeader.length === 'number' ? itemHeader.length : undefined;
153+
154+
items.push([itemHeader, binaryLength ? readBinary(binaryLength) : readJson()]);
155+
}
156+
157+
return [envelopeHeader, items];
158+
}
159+
113160
/**
114161
* Creates attachment envelope items
115162
*/

packages/utils/test/clientreport.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { ClientReport } from '@sentry/types';
2-
import { TextEncoder } from 'util';
2+
import { TextDecoder, TextEncoder } from 'util';
33

44
import { createClientReportEnvelope } from '../src/clientreport';
5-
import { serializeEnvelope } from '../src/envelope';
6-
import { parseEnvelope } from './testutils';
5+
import { parseEnvelope, serializeEnvelope } from '../src/envelope';
6+
7+
const encoder = new TextEncoder();
8+
const decoder = new TextDecoder();
79

810
const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [
911
{
@@ -44,7 +46,7 @@ describe('createClientReportEnvelope', () => {
4446
it('serializes an envelope', () => {
4547
const env = createClientReportEnvelope(DEFAULT_DISCARDED_EVENTS, MOCK_DSN, 123456);
4648

47-
const [headers, items] = parseEnvelope(serializeEnvelope(env, new TextEncoder()));
49+
const [headers, items] = parseEnvelope(serializeEnvelope(env, encoder), encoder, decoder);
4850

4951
expect(headers).toEqual({ dsn: 'https://[email protected]/1' });
5052
expect(items).toEqual([

packages/utils/test/envelope.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { EventEnvelope } from '@sentry/types';
2-
import { TextEncoder } from 'util';
2+
import { TextDecoder, TextEncoder } from 'util';
33

4-
import { addItemToEnvelope, createEnvelope, forEachEnvelopeItem, serializeEnvelope } from '../src/envelope';
5-
import { parseEnvelope } from './testutils';
4+
const encoder = new TextEncoder();
5+
const decoder = new TextDecoder();
6+
7+
import {
8+
addItemToEnvelope,
9+
createEnvelope,
10+
forEachEnvelopeItem,
11+
parseEnvelope,
12+
serializeEnvelope,
13+
} from '../src/envelope';
614

715
describe('envelope', () => {
816
describe('createEnvelope()', () => {
@@ -18,17 +26,17 @@ describe('envelope', () => {
1826
});
1927
});
2028

21-
describe('serializeEnvelope()', () => {
29+
describe('serializeEnvelope and parseEnvelope', () => {
2230
it('serializes an envelope', () => {
2331
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
24-
const serializedEnvelope = serializeEnvelope(env, new TextEncoder());
32+
const serializedEnvelope = serializeEnvelope(env, encoder);
2533
expect(typeof serializedEnvelope).toBe('string');
2634

27-
const [headers] = parseEnvelope(serializedEnvelope);
35+
const [headers] = parseEnvelope(serializedEnvelope, encoder, decoder);
2836
expect(headers).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
2937
});
3038

31-
it('serializes an envelope with attachments', () => {
39+
it.only('serializes an envelope with attachments', () => {
3240
const items: EventEnvelope[1] = [
3341
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }],
3442
[{ type: 'attachment', filename: 'bar.txt', length: 6 }, Uint8Array.from([1, 2, 3, 4, 5, 6])],
@@ -42,10 +50,10 @@ describe('envelope', () => {
4250

4351
expect.assertions(6);
4452

45-
const serializedEnvelope = serializeEnvelope(env, new TextEncoder());
53+
const serializedEnvelope = serializeEnvelope(env, encoder);
4654
expect(serializedEnvelope).toBeInstanceOf(Uint8Array);
4755

48-
const [parsedHeaders, parsedItems] = parseEnvelope(serializedEnvelope);
56+
const [parsedHeaders, parsedItems] = parseEnvelope(serializedEnvelope, encoder, decoder);
4957
expect(parsedHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
5058
expect(parsedItems).toHaveLength(3);
5159
expect(items[0]).toEqual([{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }]);
@@ -68,7 +76,7 @@ describe('envelope', () => {
6876
[{ type: 'event' }, egg],
6977
]);
7078

71-
const serializedEnvelope = serializeEnvelope(env, new TextEncoder());
79+
const serializedEnvelope = serializeEnvelope(env, encoder);
7280
const [, , serializedBody] = serializedEnvelope.toString().split('\n');
7381

7482
expect(serializedBody).toBe('{"chicken":{"egg":"[Circular ~]"}}');
@@ -78,7 +86,7 @@ describe('envelope', () => {
7886
describe('addItemToEnvelope()', () => {
7987
it('adds an item to an envelope', () => {
8088
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
81-
let [envHeaders, items] = parseEnvelope(serializeEnvelope(env, new TextEncoder()));
89+
let [envHeaders, items] = parseEnvelope(serializeEnvelope(env, encoder), encoder, decoder);
8290
expect(items).toHaveLength(0);
8391
expect(envHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
8492

@@ -87,7 +95,7 @@ describe('envelope', () => {
8795
{ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' },
8896
]);
8997

90-
[envHeaders, items] = parseEnvelope(serializeEnvelope(newEnv, new TextEncoder()));
98+
[envHeaders, items] = parseEnvelope(serializeEnvelope(newEnv, encoder), encoder, decoder);
9199
expect(envHeaders).toEqual({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' });
92100
expect(items).toHaveLength(1);
93101
expect(items[0]).toEqual([{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }]);

packages/utils/test/testutils.ts

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { BaseEnvelopeHeaders, BaseEnvelopeItemHeaders, Envelope } from '@sentry/types';
2-
import { TextDecoder, TextEncoder } from 'util';
3-
41
export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
52
const currentNodeVersion = process.env.NODE_VERSION;
63

@@ -14,56 +11,3 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
1411

1512
return it;
1613
};
17-
18-
/**
19-
* A naive binary envelope parser
20-
*/
21-
export function parseEnvelope(env: string | Uint8Array): Envelope {
22-
let buf = typeof env === 'string' ? new TextEncoder().encode(env) : env;
23-
24-
let envelopeHeaders: BaseEnvelopeHeaders | undefined;
25-
let lastItemHeader: BaseEnvelopeItemHeaders | undefined;
26-
const items: [any, any][] = [];
27-
28-
let binaryLength = 0;
29-
while (buf.length) {
30-
// Next length is either the binary length from the previous header
31-
// or the next newline character
32-
let i = binaryLength || buf.indexOf(0xa);
33-
34-
// If no newline was found, assume this is the last block
35-
if (i < 0) {
36-
i = buf.length;
37-
}
38-
39-
// If we read out a length in the previous header, assume binary
40-
if (binaryLength > 0) {
41-
const bin = buf.slice(0, binaryLength);
42-
binaryLength = 0;
43-
items.push([lastItemHeader, bin]);
44-
} else {
45-
const json = JSON.parse(new TextDecoder().decode(buf.slice(0, i + 1)));
46-
47-
if (typeof json.length === 'number') {
48-
binaryLength = json.length;
49-
}
50-
51-
// First json is always the envelope headers
52-
if (!envelopeHeaders) {
53-
envelopeHeaders = json;
54-
} else {
55-
// If there is a type property, assume this is an item header
56-
if ('type' in json) {
57-
lastItemHeader = json;
58-
} else {
59-
items.push([lastItemHeader, json]);
60-
}
61-
}
62-
}
63-
64-
// Replace the buffer with the previous block and newline removed
65-
buf = buf.slice(i + 1);
66-
}
67-
68-
return [envelopeHeaders as BaseEnvelopeHeaders, items];
69-
}

0 commit comments

Comments
 (0)