Skip to content

Commit c16c1fa

Browse files
committed
Override serialisation in node
1 parent cce0548 commit c16c1fa

File tree

16 files changed

+89
-97
lines changed

16 files changed

+89
-97
lines changed

packages/browser/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ const baseConfig = require('../../jest/jest.config.js');
22

33
module.exports = {
44
...baseConfig,
5-
testEnvironment: 'jsdom',
5+
testEnvironment: './jest.env.js',
66
testMatch: ['<rootDir>/test/unit/**/*.test.ts'],
77
};

packages/browser/jest.env.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const Environment = require('jest-environment-jsdom');
2+
3+
// Looks like jsdom does not support global TextEncoder/TextDecoder
4+
// https://github.com/jsdom/jsdom/issues/2524
5+
6+
module.exports = class CustomTestEnvironment extends Environment {
7+
async setup() {
8+
await super.setup();
9+
if (typeof this.global.TextEncoder === 'undefined') {
10+
const { TextEncoder, TextDecoder } = require('util');
11+
this.global.TextEncoder = TextEncoder;
12+
this.global.TextDecoder = TextDecoder;
13+
}
14+
}
15+
};

packages/core/src/transports/base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function createTransport(
6969
};
7070

7171
const requestTask = (): PromiseLike<void> =>
72-
makeRequest({ body: serializeEnvelope(filteredEnvelope) }).then(
72+
makeRequest({ body: (options.serializeEnvelope || serializeEnvelope)(filteredEnvelope) }).then(
7373
({ headers }): void => {
7474
if (headers) {
7575
rateLimits = updateRateLimits(rateLimits, headers);

packages/hub/src/scope.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-disable max-lines */
22
import {
33
Attachment,
4-
AttachmentOptions,
54
Breadcrumb,
65
CaptureContext,
76
Context,
@@ -408,8 +407,8 @@ export class Scope implements ScopeInterface {
408407
/**
409408
* @inheritDoc
410409
*/
411-
public addAttachment(pathOrData: string | Uint8Array, options?: AttachmentOptions): this {
412-
this._attachments.push([pathOrData, options]);
410+
public addAttachment(attachment: Attachment): this {
411+
this._attachments.push(attachment);
413412
return this;
414413
}
415414

packages/node/src/client.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,12 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
158158
protected _attachmentsFromScope(scope: Scope | undefined): AttachmentItem[] {
159159
return (
160160
scope?.getAttachments()?.map(attachment => {
161-
let [pathOrData, options] = attachment;
162-
163-
if (typeof pathOrData === 'string' && existsSync(pathOrData)) {
164-
options = options || {};
165-
options.filename = basename(pathOrData);
166-
pathOrData = readFileSync(pathOrData);
161+
if (attachment.path && existsSync(attachment.path)) {
162+
attachment.filename = basename(attachment.path);
163+
attachment.data = readFileSync(attachment.path);
167164
}
168165

169-
return createAttachmentEnvelopeItem([pathOrData, options]);
166+
return createAttachmentEnvelopeItem(attachment);
170167
}) || []
171168
);
172169
}

packages/node/src/transports/http.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TransportRequest,
77
TransportRequestExecutor,
88
} from '@sentry/types';
9+
import { serializeEnvelope } from '@sentry/utils';
910
import * as http from 'http';
1011
import * as https from 'https';
1112
import { Readable, Writable } from 'stream';
@@ -45,6 +46,8 @@ function streamFromBody(body: Uint8Array | string): Readable {
4546
* Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
4647
*/
4748
export function makeNodeTransport(options: NodeTransportOptions): Transport {
49+
options.serializeEnvelope = s => serializeEnvelope(s, new TextEncoder());
50+
4851
const urlSegments = new URL(options.url);
4952
const isHttps = urlSegments.protocol === 'https:';
5053

packages/node/test/transports/http.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createTransport } from '@sentry/core';
22
import { EventEnvelope, EventItem } from '@sentry/types';
33
import { createEnvelope, serializeEnvelope } from '@sentry/utils';
44
import * as http from 'http';
5+
import { TextEncoder, TextDecoder } from 'util';
56

67
import { makeNodeTransport } from '../../src/transports';
78

@@ -66,7 +67,7 @@ const EVENT_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4b
6667
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
6768
]);
6869

69-
const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE);
70+
const SERIALIZED_EVENT_ENVELOPE = new TextDecoder().decode(serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()));
7071

7172
const defaultOptions = {
7273
url: TEST_SERVER_URL,

packages/node/test/transports/https.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EventEnvelope, EventItem } from '@sentry/types';
33
import { createEnvelope, serializeEnvelope } from '@sentry/utils';
44
import * as http from 'http';
55
import * as https from 'https';
6+
import { TextEncoder } from 'util';
67

78
import { makeNodeTransport } from '../../src/transports';
89
import { HTTPModule, HTTPModuleRequestIncomingMessage } from '../../src/transports/http-module';
@@ -69,7 +70,7 @@ const EVENT_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4b
6970
[{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem,
7071
]);
7172

72-
const SERIALIZED_EVENT_ENVELOPE = serializeEnvelope(EVENT_ENVELOPE);
73+
const SERIALIZED_EVENT_ENVELOPE = new TextDecoder().decode(serializeEnvelope(EVENT_ENVELOPE, new TextEncoder()));
7374

7475
const unsafeHttpsModule: HTTPModule = {
7576
request: jest

packages/types/src/attachment.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
export interface AttachmentOptions {
1+
export interface Attachment {
2+
path?: string;
3+
data?: string | Uint8Array;
24
filename?: string;
35
contentType?: string;
46
attachmentType?: string;
57
}
6-
7-
export type Attachment = [string | Uint8Array, AttachmentOptions | undefined];

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type { Attachment, AttachmentOptions } from './attachment';
1+
export type { Attachment } from './attachment';
22
export type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
33
export type { Client } from './client';
44
export type { ClientReport, Outcome, EventDropReason } from './clientreport';

packages/types/src/scope.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Attachment, AttachmentOptions } from './attachment';
1+
import { Attachment } from './attachment';
22
import { Breadcrumb } from './breadcrumb';
33
import { Context, Contexts } from './context';
44
import { EventProcessor } from './eventprocessor';
@@ -162,10 +162,9 @@ export interface Scope {
162162

163163
/**
164164
* Adds an attachment to the scope
165-
* @param pathOrData A Uint8Array containing the attachment bytes
166-
* @param options Attachment options
165+
* @param attachment Attachment options
167166
*/
168-
addAttachment(pathOrData: string | Uint8Array, options?: AttachmentOptions): this;
167+
addAttachment(attachment: Attachment): this;
169168

170169
/**
171170
* Returns an array of attachments on the scope

packages/types/src/transport.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type TransportMakeRequestResponse = {
1717
export interface InternalBaseTransportOptions {
1818
bufferSize?: number;
1919
recordDroppedEvent: (reason: EventDropReason, dataCategory: DataCategory) => void;
20+
serializeEnvelope?: (env: Envelope) => Uint8Array;
2021
}
2122

2223
export interface BaseTransportOptions extends InternalBaseTransportOptions {

packages/utils/src/envelope.ts

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { Attachment, AttachmentItem, DataCategory, Envelope, EnvelopeItem, EnvelopeItemType } from '@sentry/types';
22

3-
import { isPrimitive } from './is';
4-
53
/**
64
* Creates an envelope.
75
* Make sure to always explicitly provide the generic to this function
@@ -36,61 +34,48 @@ export function forEachEnvelopeItem<E extends Envelope>(
3634
});
3735
}
3836

39-
/**
40-
* Serializes an envelope.
41-
*/
42-
export function serializeEnvelope(envelope: Envelope): string | Uint8Array {
43-
const [, items] = envelope;
44-
45-
// Have to cast items to any here since Envelope is a union type
46-
// Fixed in Typescript 4.2
47-
// TODO: Remove any[] cast when we upgrade to TS 4.2
48-
// https://github.com/microsoft/TypeScript/issues/36390
49-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
50-
const hasBinaryAttachment = (items as any[]).some(
51-
(item: typeof items[number]) => item[0].type === 'attachment' && item[1] instanceof Uint8Array,
52-
);
53-
54-
return hasBinaryAttachment ? serializeBinaryEnvelope(envelope) : serializeStringEnvelope(envelope);
37+
// Cached string encoder
38+
let encoder: TextEncoder | undefined;
39+
40+
function getCachedEncoder(): TextEncoder {
41+
if (!encoder) {
42+
encoder = new TextEncoder();
43+
}
44+
45+
return encoder;
46+
}
47+
48+
// Global TextEncoder and Node require('util').TextEncoder
49+
interface TextEncoderInternal extends TextEncoderCommon {
50+
encode(input?: string): Uint8Array;
5551
}
5652

5753
/**
58-
* Serializes an envelope to a string.
54+
* Serializes an envelope.
5955
*/
60-
export function serializeStringEnvelope(envelope: Envelope): string {
61-
const [headers, items] = envelope;
62-
const serializedHeaders = JSON.stringify(headers);
63-
64-
// TODO: Remove any[] cast when we upgrade to TS 4.2
65-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
66-
return (items as any[]).reduce((acc, item: typeof items[number]) => {
67-
const [itemHeaders, payload] = item;
68-
// We do not serialize payloads that are primitives
69-
const serializedPayload = isPrimitive(payload) ? String(payload) : JSON.stringify(payload);
70-
return `${acc}\n${JSON.stringify(itemHeaders)}\n${serializedPayload}`;
71-
}, serializedHeaders);
72-
}
56+
export function serializeEnvelope(envelope: Envelope, textEncoderOverride?: TextEncoderInternal): Uint8Array {
57+
const textEncoder = textEncoderOverride || getCachedEncoder();
7358

74-
function serializeBinaryEnvelope(envelope: Envelope): Uint8Array {
75-
const encoder = new TextEncoder();
7659
const [headers, items] = envelope;
77-
const serializedHeaders = JSON.stringify(headers);
7860

79-
const chunks = [encoder.encode(serializedHeaders)];
61+
const parts = [textEncoder.encode(JSON.stringify(headers))];
8062

8163
for (const item of items) {
8264
const [itemHeaders, payload] = item as typeof items[number];
83-
chunks.push(encoder.encode(`\n${JSON.stringify(itemHeaders)}\n`));
65+
parts.push(textEncoder.encode(`\n${JSON.stringify(itemHeaders)}\n`));
66+
8467
if (typeof payload === 'string') {
85-
chunks.push(encoder.encode(payload));
68+
parts.push(textEncoder.encode(payload));
8669
} else if (payload instanceof Uint8Array) {
87-
chunks.push(payload);
70+
parts.push(payload);
8871
} else {
89-
chunks.push(encoder.encode(JSON.stringify(payload)));
72+
parts.push(textEncoder.encode(JSON.stringify(payload)));
9073
}
9174
}
9275

93-
return concatBuffers(chunks);
76+
parts.push(textEncoder.encode('\n'));
77+
78+
return concatBuffers(parts);
9479
}
9580

9681
function concatBuffers(buffers: Uint8Array[]): Uint8Array {
@@ -110,19 +95,17 @@ function concatBuffers(buffers: Uint8Array[]): Uint8Array {
11095
* Creates attachment envelope items
11196
*/
11297
export function createAttachmentEnvelopeItem(attachment: Attachment): AttachmentItem {
113-
const [pathOrData, options] = attachment;
114-
115-
const buffer = typeof pathOrData === 'string' ? new TextEncoder().encode(pathOrData) : pathOrData;
98+
const buffer = typeof attachment.data === 'string' ? new TextEncoder().encode(attachment.data) : attachment.data;
11699

117100
return [
118101
{
119102
type: 'attachment',
120-
length: buffer.length,
121-
filename: options?.filename || 'No filename',
122-
content_type: options?.contentType,
123-
attachment_type: options?.attachmentType,
103+
length: buffer?.length || 0,
104+
filename: attachment?.filename || 'No filename',
105+
content_type: attachment?.contentType,
106+
attachment_type: attachment?.attachmentType,
124107
},
125-
buffer,
108+
buffer || new Uint8Array(0),
126109
];
127110
}
128111

packages/utils/test/clientreport.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ describe('createClientReportEnvelope', () => {
4242
it('serializes an envelope', () => {
4343
const env = createClientReportEnvelope(DEFAULT_DISCARDED_EVENTS, MOCK_DSN, 123456);
4444
const serializedEnv = serializeEnvelope(env);
45-
expect(serializedEnv).toMatchInlineSnapshot(`
46-
"{\\"dsn\\":\\"https://public@example.com/1\\"}
47-
{\\"type\\":\\"client_report\\"}
48-
{\\"timestamp\\":123456,\\"discarded_events\\":[{\\"reason\\":\\"before_send\\",\\"category\\":\\"error\\",\\"quantity\\":30},{\\"reason\\":\\"network_error\\",\\"category\\":\\"transaction\\",\\"quantity\\":23}]}"
49-
`);
45+
46+
const strEnvelope = new TextDecoder().decode(serializedEnv);
47+
48+
expect(strEnvelope).toMatch(`{"dsn":"https://public@example.com/1"}
49+
{"type":"client_report"}
50+
{"timestamp":123456,"discarded_events":[{"reason":"before_send","category":"error","quantity":30},{"reason":"network_error","category":"transaction","quantity":23}]}
51+
`);
5052
});
5153
});

packages/utils/test/envelope.test.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { EventEnvelope } from '@sentry/types';
22

3-
import {
4-
addItemToEnvelope,
5-
createEnvelope,
6-
forEachEnvelopeItem,
7-
serializeStringEnvelope,
8-
serializeEnvelope,
9-
} from '../src/envelope';
3+
import { addItemToEnvelope, createEnvelope, forEachEnvelopeItem, serializeEnvelope } from '../src/envelope';
104
import { parseEnvelope } from './testutils';
115

126
describe('envelope', () => {
@@ -23,12 +17,11 @@ describe('envelope', () => {
2317
});
2418
});
2519

26-
describe('serializeStringEnvelope()', () => {
20+
describe('serializeEnvelope()', () => {
2721
it('serializes an envelope', () => {
2822
const env = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, []);
29-
expect(serializeStringEnvelope(env)).toMatchInlineSnapshot(
30-
'"{\\"event_id\\":\\"aa3ff046696b4bc6b609ce6d28fde9e2\\",\\"sent_at\\":\\"123\\"}"',
31-
);
23+
const strEnvelope = new TextDecoder().decode(serializeEnvelope(env));
24+
expect(strEnvelope).toMatch('{"event_id":"aa3ff046696b4bc6b609ce6d28fde9e2","sent_at":"123"}');
3225
});
3326
});
3427

packages/utils/test/testutils.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BaseEnvelopeHeaders, BaseEnvelopeItemHeaders,Envelope } from '@sentry/types';
1+
import { BaseEnvelopeHeaders, BaseEnvelopeItemHeaders, Envelope } from '@sentry/types';
22

33
export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
44
const currentNodeVersion = process.env.NODE_VERSION;
@@ -18,32 +18,30 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
1818
* A naive binary envelope parser
1919
*/
2020
export function parseEnvelope(env: string | Uint8Array): Envelope {
21-
if (typeof env === 'string') {
22-
env = new TextEncoder().encode(env);
23-
}
21+
let buf = typeof env === 'string' ? new TextEncoder().encode(env) : env;
2422

2523
let envelopeHeaders: BaseEnvelopeHeaders | undefined;
2624
let lastItemHeader: BaseEnvelopeItemHeaders | undefined;
2725
const items: [any, any][] = [];
2826

2927
let binaryLength = 0;
30-
while (env.length) {
28+
while (buf.length) {
3129
// Next length is either the binary length from the previous header
3230
// or the next newline character
33-
let i = binaryLength || env.indexOf(0xa);
31+
let i = binaryLength || buf.indexOf(0xa);
3432

3533
// If no newline was found, assume this is the last block
3634
if (i < 0) {
37-
i = env.length;
35+
i = buf.length;
3836
}
3937

4038
// If we read out a length in the previous header, assume binary
4139
if (binaryLength > 0) {
42-
const bin = env.slice(0, binaryLength);
40+
const bin = buf.slice(0, binaryLength);
4341
binaryLength = 0;
4442
items.push([lastItemHeader, bin]);
4543
} else {
46-
const json = JSON.parse(new TextDecoder().decode(env.slice(0, i + 1)));
44+
const json = JSON.parse(new TextDecoder().decode(buf.slice(0, i + 1)));
4745

4846
if (typeof json.length === 'number') {
4947
binaryLength = json.length;
@@ -63,7 +61,7 @@ export function parseEnvelope(env: string | Uint8Array): Envelope {
6361
}
6462

6563
// Replace the buffer with the previous block and newline removed
66-
env = env.slice(i + 1);
64+
buf = buf.slice(i + 1);
6765
}
6866

6967
return [envelopeHeaders as BaseEnvelopeHeaders, items];

0 commit comments

Comments
 (0)