Skip to content

Revamp the Transports API #4660

Closed
Closed
@AbhiPrasad

Description

@AbhiPrasad

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:

/** JSDoc */
export interface TransportOptions {
/** Sentry DSN */
dsn: DsnLike;
/** Define custom headers */
headers?: { [key: string]: string };
/** 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;
/** Fetch API init parameters */
fetchParameters?: { [key: string]: string };
/** The envelope tunnel to use. */
tunnel?: string;
/** Send SDK Client Reports. Enabled by default. */
sendClientReports?: boolean;
/**
* 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;
}

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions