Description
As part of #4240 (comment), we want to revamp the Transports API to make it easier to extend (adding support for attachments), consolidate the options, and reduce bundle size.
This issue forms a living document on how we are going to tackle this - first by setting up an API that can be (possibly) merged in before v7.
Much of this is inspired from the transport design of https://github.com/getsentry/sentry-javascript/blob/v7-dev/packages/transport-base/src/transport.ts
Options
To start, let's look at the current state of the options:
sentry-javascript/packages/types/src/transport.ts
Lines 51 to 74 in b1009b5
Doing a quick runthrough, here's what that looks like:
export interface BaseTransportOptions {
/** Sentry DSN */
dsn: DsnLike;
/** Define custom headers */
headers?: { [key: string]: string };
/** Set a HTTP proxy that should be used for outbound requests. */
httpProxy?: string; // ONLY USED BY NODE SDK
/** Set a HTTPS proxy that should be used for outbound requests. */
httpsProxy?: string; // ONLY USED BY NODE SDK
/** HTTPS proxy certificates path */
caCerts?: string; // ONLY USED BY NODE SDK
/** Fetch API init parameters */
fetchParameters?: { [key: string]: string }; // ONLY USED BY BROWSER SDK
/** The envelope tunnel to use. */
tunnel?: string;
/** Send SDK Client Reports. Enabled by default. */
sendClientReports?: boolean; // ONLY USED BY BROWSER SDK ATM
/**
* Set of metadata about the SDK that can be internally used to enhance envelopes and events,
* and provide additional data about every request.
* */
_metadata?: SdkMetadata;
}
This means we can probably reduce it down to:
export interface BaseTransportOptions {
// url to send the event
// transport does not care about dsn specific - client should take care of
// parsing and figuring that out
url: string;
headers?: Record<string, string>;
bufferSize?: number; // make transport buffer size configurable
};
export interface BrowserTransportOptions extends BaseTransportOptions {
// options to pass into fetch request
fetchParams: Record<string, string>;
sendClientReports?: boolean;
// TODO: Add custom fetch implementation?
}
export interface NodeTransportOptions extends BaseTransportOptions {
// Set a HTTP proxy that should be used for outbound requests.
httpProxy?: string;
// Set a HTTPS proxy that should be used for outbound requests.
httpsProxy?: string;
// HTTPS proxy certificates path
caCerts?: string;
}
API
The transport does a couple of things in the SDK, but we can think about it mainly turning a SentryRequest
into a Response
of some kind. Due to rate-limiting, the transport is essentially a closed-loop control system, so the Response
must be of some form to guide the transport (and the SDK in general) in making future decisions about how it should function.
SentryRequest<T>
-> SentryResponse
. SentryRequest
becomes generic (defaulting to T = string
), so transports can work with buffers/event-emitters/streams as the body if it so pleases. This also leaves us open to have non-http transports (although we'll never probably have that as a default).
Assuming that in v7, the client is in charge of creating the envelope (because it has to be able to dynamically add items).
interface INewTransport {
// TODO: How to best attach the type?
send(request: Envelope, type: SentryRequestType): PromiseLike<TransportResponse>;
flush(timeout: number): PromiseLike<boolean>;
}
We can actually make this entirely functional
function createTransport(options: TransportOptions): INewTransport {
// some rate limiting logic
// some transport buffer logic
// this is a huge breaking change though
// makes it way harder for users to implement their own transports
// but is that a case we really care about?
return { send, flush };
}
Sticking with classes though
/**
* Heavily based on Kamil's work in
* https://github.com/getsentry/sentry-javascript/blob/v7-dev/packages/transport-base/src/transport.ts
*/
export abstract class BaseTransport implements INewTransport {
protected readonly _buffer: PromiseBuffer<TransportResponse>;
private readonly _rateLimits: Record<string, number> = {};
public constructor(protected readonly _options: BaseTransportOptions) {
this._buffer = makePromiseBuffer(this._options.bufferSize || 30);
}
/** */
public send(envelope: Envelope, type: SentryRequestType): PromiseLike<TransportResponse> {
const request: TransportRequest = {
// I'm undecided if the type API should work like this
// though we are a little stuck with this because of how
// minimal the envelopes implementation is
// perhaps there is a way we can expand it?
type,
body: serializeEnvelope(envelope),
};
if (isRateLimited(this._rateLimits, type)) {
return rejectedSyncPromise(
new SentryError(`oh no, disabled until: ${rateLimitDisableUntil(this._rateLimits, type)}`),
);
}
const requestTask = (): PromiseLike<TransportResponse> =>
this._makeRequest(request).then(({ body, headers, reason, statusCode }): PromiseLike<TransportResponse> => {
if (headers) {
updateRateLimits(this._rateLimits, headers);
}
// TODO: This is the happy path!
const status = eventStatusFromHttpCode(statusCode);
if (status === 'success') {
return resolvedSyncPromise({ status });
}
return rejectedSyncPromise(new SentryError(body || reason || 'Unknown transport error'));
});
return this._buffer.add(requestTask);
}
/** */
public flush(timeout?: number): PromiseLike<boolean> {
return this._buffer.drain(timeout);
}
// Each is up to each transport implementation to determine how to make a request -> return an according response
// `TransportMakeRequestResponse` is different than `TransportResponse` because the client doesn't care about
// these extra details
protected abstract _makeRequest(request: TransportRequest): PromiseLike<TransportMakeRequestResponse>;
}
Edit: Have to incorporate client outcomes here - will figure that out in a bit.