Skip to content

Commit ed65564

Browse files
committed
Mostly there
1 parent c55003f commit ed65564

File tree

14 files changed

+174
-20
lines changed

14 files changed

+174
-20
lines changed

packages/browser/src/client.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BaseClient, Scope, SDK_VERSION } from '@sentry/core';
2-
import { ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types';
3-
import { getGlobalObject, logger } from '@sentry/utils';
2+
import { AttachmentItem, ClientOptions, Event, EventHint, Options, Severity, SeverityLevel } from '@sentry/types';
3+
import { attachmentItemFromAttachment, getGlobalObject, logger } from '@sentry/utils';
44

55
import { eventFromException, eventFromMessage } from './eventbuilder';
66
import { IS_DEBUG_BUILD } from './flags';
@@ -115,11 +115,18 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
115115
/**
116116
* @inheritDoc
117117
*/
118-
protected _sendEvent(event: Event): void {
118+
protected _sendEvent(event: Event, attachments: AttachmentItem[]): void {
119119
const integration = this.getIntegration(Breadcrumbs);
120120
if (integration) {
121121
integration.addSentryBreadcrumb(event);
122122
}
123-
super._sendEvent(event);
123+
super._sendEvent(event, attachments);
124+
}
125+
126+
/**
127+
* @inheritDoc
128+
*/
129+
protected _getAttachments(scope: Scope | undefined): AttachmentItem[] {
130+
return (scope?.getAttachments() || []).map(a => attachmentItemFromAttachment(a));
124131
}
125132
}

packages/core/src/baseclient.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable max-lines */
22
import { Scope, Session } from '@sentry/hub';
33
import {
4+
AttachmentItem,
45
Client,
56
ClientOptions,
67
DsnComponents,
@@ -263,9 +264,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
263264
/**
264265
* @inheritDoc
265266
*/
266-
public sendEvent(event: Event): void {
267+
public sendEvent(event: Event, attachments?: AttachmentItem[]): void {
267268
if (this._dsn) {
268-
const env = createEventEnvelope(event, this._dsn, this._options._metadata, this._options.tunnel);
269+
const env = createEventEnvelope(event, this._dsn, attachments, this._options._metadata, this._options.tunnel);
269270
this.sendEnvelope(env);
270271
}
271272
}
@@ -525,8 +526,8 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
525526
* @param event The Sentry event to send
526527
*/
527528
// TODO(v7): refactor: get rid of method?
528-
protected _sendEvent(event: Event): void {
529-
this.sendEvent(event);
529+
protected _sendEvent(event: Event, attachments: AttachmentItem[]): void {
530+
this.sendEvent(event, attachments);
530531
}
531532

532533
/**
@@ -618,7 +619,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
618619
this._updateSessionFromEvent(session, processedEvent);
619620
}
620621

621-
this._sendEvent(processedEvent);
622+
this._sendEvent(processedEvent, this._getAttachments(scope));
622623
return processedEvent;
623624
})
624625
.then(null, reason => {
@@ -670,6 +671,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
670671
_level?: Severity | SeverityLevel,
671672
_hint?: EventHint,
672673
): PromiseLike<Event>;
674+
675+
/** */
676+
protected abstract _getAttachments(scope: Scope | undefined): AttachmentItem[];
673677
}
674678

675679
/**

packages/core/src/envelope.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AttachmentItem,
23
DsnComponents,
34
Event,
45
EventEnvelope,
@@ -68,6 +69,7 @@ export function createSessionEnvelope(
6869
export function createEventEnvelope(
6970
event: Event,
7071
dsn: DsnComponents,
72+
attachments: AttachmentItem[] = [],
7173
metadata?: SdkMetadata,
7274
tunnel?: string,
7375
): EventEnvelope {
@@ -119,5 +121,5 @@ export function createEventEnvelope(
119121
},
120122
event,
121123
];
122-
return createEnvelope<EventEnvelope>(envelopeHeaders, [eventItem]);
124+
return createEnvelope<EventEnvelope>(envelopeHeaders, [eventItem, ...attachments]);
123125
}

packages/hub/src/scope.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/* eslint-disable max-lines */
22
import {
3+
Attachment,
4+
AttachmentOptions,
35
Breadcrumb,
46
CaptureContext,
57
Context,
@@ -85,6 +87,9 @@ export class Scope implements ScopeInterface {
8587
/** Request Mode Session Status */
8688
protected _requestSession?: RequestSession;
8789

90+
/** Attachments */
91+
protected _attachments: Attachment[] = [];
92+
8893
/**
8994
* A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get
9095
* sent to Sentry
@@ -110,6 +115,7 @@ export class Scope implements ScopeInterface {
110115
newScope._fingerprint = scope._fingerprint;
111116
newScope._eventProcessors = [...scope._eventProcessors];
112117
newScope._requestSession = scope._requestSession;
118+
newScope._attachments = [...scope._attachments];
113119
}
114120
return newScope;
115121
}
@@ -365,6 +371,7 @@ export class Scope implements ScopeInterface {
365371
this._span = undefined;
366372
this._session = undefined;
367373
this._notifyScopeListeners();
374+
this._attachments = [];
368375
return this;
369376
}
370377

@@ -398,6 +405,29 @@ export class Scope implements ScopeInterface {
398405
return this;
399406
}
400407

408+
/**
409+
* @inheritDoc
410+
*/
411+
public addAttachment(pathOrData: string | Uint8Array, options?: AttachmentOptions): this {
412+
this._attachments.push([pathOrData, options]);
413+
return this;
414+
}
415+
416+
/**
417+
* @inheritDoc
418+
*/
419+
public getAttachments(): Attachment[] {
420+
return this._attachments;
421+
}
422+
423+
/**
424+
* @inheritDoc
425+
*/
426+
public clearAttachments(): this {
427+
this._attachments = [];
428+
return this;
429+
}
430+
401431
/**
402432
* Applies the current context and fingerprint to the event.
403433
* Note that breadcrumbs will be added by the client.

packages/node/src/client.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BaseClient, Scope, SDK_VERSION } from '@sentry/core';
22
import { SessionFlusher } from '@sentry/hub';
3-
import { Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
4-
import { logger, resolvedSyncPromise } from '@sentry/utils';
3+
import { AttachmentItem, Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
4+
import { attachmentItemFromAttachment, logger, resolvedSyncPromise } from '@sentry/utils';
55

66
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
77
import { IS_DEBUG_BUILD } from './flags';
@@ -150,4 +150,12 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
150150
this._sessionFlusher.incrementSessionStatusCount();
151151
}
152152
}
153+
154+
/**
155+
* @inheritDoc
156+
*/
157+
protected _getAttachments(scope: Scope | undefined): AttachmentItem[] {
158+
// TODO: load attachment from path...
159+
return (scope?.getAttachments() || []).map(a => attachmentItemFromAttachment(a));
160+
}
153161
}

packages/node/src/transports/http.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
import { eventStatusFromHttpCode } from '@sentry/utils';
1010
import * as http from 'http';
1111
import * as https from 'https';
12+
import { Readable, Writable } from 'stream';
1213
import { URL } from 'url';
14+
import { createGzip } from 'zlib';
1315

1416
import { HTTPModule } from './http-module';
1517

@@ -24,6 +26,22 @@ export interface NodeTransportOptions extends BaseTransportOptions {
2426
httpModule?: HTTPModule;
2527
}
2628

29+
// Estimated maximum size for reasonable standalone event
30+
const GZIP_THRESHOLD = 1024 * 32;
31+
32+
/**
33+
* Gets a stream from a Uint8Array or string
34+
* We don't have Readable.from in earlier versions of node
35+
*/
36+
function streamFromBody(body: Uint8Array | string): Readable {
37+
return new Readable({
38+
read() {
39+
this.push(body);
40+
this.push(null);
41+
},
42+
});
43+
}
44+
2745
/**
2846
* Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
2947
*/
@@ -86,6 +104,14 @@ function createRequestExecutor(
86104
const { hostname, pathname, port, protocol, search } = new URL(options.url);
87105
return function makeRequest(request: TransportRequest): Promise<TransportMakeRequestResponse> {
88106
return new Promise((resolve, reject) => {
107+
let bodyStream = streamFromBody(request.body);
108+
109+
if (request.body.length > GZIP_THRESHOLD) {
110+
options.headers = options.headers || {};
111+
options.headers['Content-Encoding'] = 'gzip';
112+
bodyStream = bodyStream.pipe(createGzip());
113+
}
114+
89115
const req = httpModule.request(
90116
{
91117
method: 'POST',
@@ -128,7 +154,9 @@ function createRequestExecutor(
128154
);
129155

130156
req.on('error', reject);
131-
req.end(request.body);
157+
158+
// The docs say that HTTPModuleClientRequest is Writable but the types don't match exactly
159+
bodyStream.pipe(req as unknown as Writable);
132160
});
133161
};
134162
}

packages/types/src/attachment.ts

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

packages/types/src/envelope.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ type EventItemHeaders = {
3030
type: 'event' | 'transaction';
3131
sample_rates?: [{ id?: TransactionSamplingMethod; rate?: number }];
3232
};
33-
type AttachmentItemHeaders = { type: 'attachment'; filename: string };
33+
type AttachmentItemHeaders = {
34+
type: 'attachment';
35+
length: number;
36+
filename: string;
37+
content_type?: string;
38+
attachment_type?: string;
39+
};
3440
type UserFeedbackItemHeaders = { type: 'user_report' };
3541
type SessionItemHeaders = { type: 'session' };
3642
type SessionAggregatesItemHeaders = { type: 'sessions' };
@@ -40,7 +46,7 @@ type ClientReportItemHeaders = { type: 'client_report' };
4046
// We have to allow this hack for now as we pre-serialize events because we support
4147
// both store and envelope endpoints.
4248
export type EventItem = BaseEnvelopeItem<EventItemHeaders, Event | string>;
43-
export type AttachmentItem = BaseEnvelopeItem<AttachmentItemHeaders, unknown>;
49+
export type AttachmentItem = BaseEnvelopeItem<AttachmentItemHeaders, Uint8Array>;
4450
export type UserFeedbackItem = BaseEnvelopeItem<UserFeedbackItemHeaders, UserFeedback>;
4551
export type SessionItem =
4652
| BaseEnvelopeItem<SessionItemHeaders, Session>

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export type { Attachment, AttachmentOptions } from './attachment';
12
export type { Breadcrumb, BreadcrumbHint } from './breadcrumb';
23
export type { Client } from './client';
34
export type { ClientReport } from './clientreport';

packages/types/src/scope.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Attachment, AttachmentOptions } from './attachment';
12
import { Breadcrumb } from './breadcrumb';
23
import { Context, Contexts } from './context';
34
import { EventProcessor } from './eventprocessor';
@@ -158,4 +159,10 @@ export interface Scope {
158159
* Clears all currently set Breadcrumbs.
159160
*/
160161
clearBreadcrumbs(): this;
162+
163+
addAttachment(pathOrData: string | Uint8Array, options?: AttachmentOptions): this;
164+
165+
getAttachments(): Attachment[];
166+
167+
clearAttachments(): this;
161168
}

packages/types/src/transport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type Outcome =
1212
export type TransportCategory = 'error' | 'transaction' | 'attachment' | 'session';
1313

1414
export type TransportRequest = {
15-
body: string;
15+
body: string | Uint8Array;
1616
category: TransportCategory;
1717
};
1818

packages/utils/src/attachment.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Attachment, AttachmentItem } from '@sentry/types';
2+
3+
/** */
4+
export function attachmentItemFromAttachment(_attachment: Attachment): AttachmentItem {
5+
throw new Error('Not implemented');
6+
}

packages/utils/src/envelope.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,68 @@ export function getEnvelopeType<E extends Envelope>(envelope: E): string {
3030
}
3131

3232
/**
33-
* Serializes an envelope into a string.
33+
* Serializes an envelope.
3434
*/
35-
export function serializeEnvelope(envelope: Envelope): string {
36-
const [headers, items] = envelope;
37-
const serializedHeaders = JSON.stringify(headers);
35+
export function serializeEnvelope(envelope: Envelope): string | Uint8Array {
36+
const [, items] = envelope;
3837

3938
// Have to cast items to any here since Envelope is a union type
4039
// Fixed in Typescript 4.2
4140
// TODO: Remove any[] cast when we upgrade to TS 4.2
4241
// https://github.com/microsoft/TypeScript/issues/36390
4342
// eslint-disable-next-line @typescript-eslint/no-explicit-any
43+
const hasBinaryAttachment = (items as any[]).some(
44+
(item: typeof items[number]) => item[0].type === 'attachment' && item[1] instanceof Uint8Array,
45+
);
46+
47+
return hasBinaryAttachment ? serializeBinaryEnvelope(envelope) : serializeStringEnvelope(envelope);
48+
}
49+
50+
function serializeStringEnvelope(envelope: Envelope): string {
51+
const [headers, items] = envelope;
52+
const serializedHeaders = JSON.stringify(headers);
53+
54+
// TODO: Remove any[] cast when we upgrade to TS 4.2
55+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4456
return (items as any[]).reduce((acc, item: typeof items[number]) => {
4557
const [itemHeaders, payload] = item;
4658
// We do not serialize payloads that are primitives
4759
const serializedPayload = isPrimitive(payload) ? String(payload) : JSON.stringify(payload);
4860
return `${acc}\n${JSON.stringify(itemHeaders)}\n${serializedPayload}`;
4961
}, serializedHeaders);
5062
}
63+
64+
function serializeBinaryEnvelope(envelope: Envelope): Uint8Array {
65+
const encoder = new TextEncoder();
66+
const [headers, items] = envelope;
67+
const serializedHeaders = JSON.stringify(headers);
68+
69+
const chunks = [encoder.encode(serializedHeaders)];
70+
71+
for (const item of items) {
72+
const [itemHeaders, payload] = item as typeof items[number];
73+
chunks.push(encoder.encode(`\n${JSON.stringify(itemHeaders)}\n`));
74+
if (typeof payload === 'string') {
75+
chunks.push(encoder.encode(payload));
76+
} else if (payload instanceof Uint8Array) {
77+
chunks.push(payload);
78+
} else {
79+
chunks.push(encoder.encode(JSON.stringify(payload)));
80+
}
81+
}
82+
83+
return concatBuffers(chunks);
84+
}
85+
86+
function concatBuffers(buffers: Uint8Array[]): Uint8Array {
87+
const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0);
88+
89+
const merged = new Uint8Array(totalLength);
90+
let offset = 0;
91+
for (const buffer of buffers) {
92+
merged.set(buffer, offset);
93+
offset += buffer.length;
94+
}
95+
96+
return merged;
97+
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ export * from './env';
2424
export * from './envelope';
2525
export * from './clientreport';
2626
export * from './ratelimit';
27+
export * from './attachment';

0 commit comments

Comments
 (0)