Skip to content

Commit 76acf2f

Browse files
authored
feat(core): Introduce New Transports API (#4716)
This PR introduces a functional transport based on what was discussed in #4660. The transport is defined by an interface, which sets two basic functions, `send` and `flush`, which both interact directly with an internal `PromiseBuffer` data structure, a queue of requests that represents the data that needs to be sent to Sentry. ```ts interface NewTransport { // If `$` is set, we know that this is a new transport. // TODO(v7): Remove this as we will no longer have split between // old and new transports. $: boolean; send(request: Envelope, category: TransportCategory): PromiseLike<TransportResponse>; flush(timeout: number): PromiseLike<boolean>; } ``` `send` relies on an externally defined `makeRequest` function (that is passed into a `makeTransport` constructor) to make a request, and then return a status based on it. It also updates internal rate-limits according to the response from `makeRequest`. The status will be also used by the client in the future for client outcomes (we will extract client outcomes from the transport -> client because it living in the transport is not the best pattern). `send` takes in an envelope, which means that for now, no errors will go through this transport when we update the client to use this new transport. `flush`, flushes the promise buffer, pretty much how it worked before. To make a custom transport (and how we'll be making fetch, xhr transports), you essentially define a `makeRequest` function. ```ts function createFetchTransport(options: FetchTransportOptions): NewTransport { function makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse> { return fetch(options.url, options.fetchOptions).then(response => { return response.text().then(body => ({ body, headers: { 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), 'retry-after': response.headers.get('Retry-After'), }, reason: response.statusText, statusCode: response.status, })); }); } return createTransport({ bufferSize: options.bufferSize }, makeRequest); } ```
1 parent cf16e48 commit 76acf2f

File tree

2 files changed

+525
-0
lines changed

2 files changed

+525
-0
lines changed

packages/core/src/transports/base.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { Envelope, EventStatus } from '@sentry/types';
2+
import {
3+
disabledUntil,
4+
eventStatusFromHttpCode,
5+
isRateLimited,
6+
makePromiseBuffer,
7+
PromiseBuffer,
8+
RateLimits,
9+
rejectedSyncPromise,
10+
resolvedSyncPromise,
11+
serializeEnvelope,
12+
updateRateLimits,
13+
} from '@sentry/utils';
14+
15+
export const ERROR_TRANSPORT_CATEGORY = 'error';
16+
17+
export const TRANSACTION_TRANSPORT_CATEGORY = 'transaction';
18+
19+
export const ATTACHMENT_TRANSPORT_CATEGORY = 'attachment';
20+
21+
export const SESSION_TRANSPORT_CATEGORY = 'session';
22+
23+
type TransportCategory =
24+
| typeof ERROR_TRANSPORT_CATEGORY
25+
| typeof TRANSACTION_TRANSPORT_CATEGORY
26+
| typeof ATTACHMENT_TRANSPORT_CATEGORY
27+
| typeof SESSION_TRANSPORT_CATEGORY;
28+
29+
export type TransportRequest = {
30+
body: string;
31+
category: TransportCategory;
32+
};
33+
34+
export type TransportMakeRequestResponse = {
35+
body?: string;
36+
headers?: {
37+
[key: string]: string | null;
38+
'x-sentry-rate-limits': string | null;
39+
'retry-after': string | null;
40+
};
41+
reason?: string;
42+
statusCode: number;
43+
};
44+
45+
export type TransportResponse = {
46+
status: EventStatus;
47+
reason?: string;
48+
};
49+
50+
interface InternalBaseTransportOptions {
51+
bufferSize?: number;
52+
}
53+
54+
export interface BaseTransportOptions extends InternalBaseTransportOptions {
55+
// url to send the event
56+
// transport does not care about dsn specific - client should take care of
57+
// parsing and figuring that out
58+
url: string;
59+
}
60+
61+
// TODO: Move into Browser Transport
62+
export interface BrowserTransportOptions extends BaseTransportOptions {
63+
// options to pass into fetch request
64+
fetchParams: Record<string, string>;
65+
headers?: Record<string, string>;
66+
sendClientReports?: boolean;
67+
}
68+
69+
// TODO: Move into Node transport
70+
export interface NodeTransportOptions extends BaseTransportOptions {
71+
// Set a HTTP proxy that should be used for outbound requests.
72+
httpProxy?: string;
73+
// Set a HTTPS proxy that should be used for outbound requests.
74+
httpsProxy?: string;
75+
// HTTPS proxy certificates path
76+
caCerts?: string;
77+
}
78+
79+
export interface NewTransport {
80+
// If `$` is set, we know that this is a new transport.
81+
// TODO(v7): Remove this as we will no longer have split between
82+
// old and new transports.
83+
$: boolean;
84+
send(request: Envelope, category: TransportCategory): PromiseLike<TransportResponse>;
85+
flush(timeout?: number): PromiseLike<boolean>;
86+
}
87+
88+
export type TransportRequestExecutor = (request: TransportRequest) => PromiseLike<TransportMakeRequestResponse>;
89+
90+
export const DEFAULT_TRANSPORT_BUFFER_SIZE = 30;
91+
92+
/**
93+
* Creates a `NewTransport`
94+
*
95+
* @param options
96+
* @param makeRequest
97+
*/
98+
export function createTransport(
99+
options: InternalBaseTransportOptions,
100+
makeRequest: TransportRequestExecutor,
101+
buffer: PromiseBuffer<TransportResponse> = makePromiseBuffer(options.bufferSize || DEFAULT_TRANSPORT_BUFFER_SIZE),
102+
): NewTransport {
103+
let rateLimits: RateLimits = {};
104+
105+
const flush = (timeout?: number): PromiseLike<boolean> => buffer.drain(timeout);
106+
107+
function send(envelope: Envelope, category: TransportCategory): PromiseLike<TransportResponse> {
108+
const request: TransportRequest = {
109+
category,
110+
body: serializeEnvelope(envelope),
111+
};
112+
113+
// Don't add to buffer if transport is already rate-limited
114+
if (isRateLimited(rateLimits, category)) {
115+
return rejectedSyncPromise({
116+
status: 'rate_limit',
117+
reason: getRateLimitReason(rateLimits, category),
118+
});
119+
}
120+
121+
const requestTask = (): PromiseLike<TransportResponse> =>
122+
makeRequest(request).then(({ body, headers, reason, statusCode }): PromiseLike<TransportResponse> => {
123+
const status = eventStatusFromHttpCode(statusCode);
124+
if (headers) {
125+
rateLimits = updateRateLimits(rateLimits, headers);
126+
}
127+
if (status === 'success') {
128+
return resolvedSyncPromise({ status, reason });
129+
}
130+
return rejectedSyncPromise({
131+
status,
132+
reason:
133+
reason ||
134+
body ||
135+
(status === 'rate_limit' ? getRateLimitReason(rateLimits, category) : 'Unknown transport error'),
136+
});
137+
});
138+
139+
return buffer.add(requestTask);
140+
}
141+
142+
return {
143+
$: true,
144+
send,
145+
flush,
146+
};
147+
}
148+
149+
function getRateLimitReason(rateLimits: RateLimits, category: TransportCategory): string {
150+
return `Too many ${category} requests, backing off until: ${new Date(
151+
disabledUntil(rateLimits, category),
152+
).toISOString()}`;
153+
}

0 commit comments

Comments
 (0)