Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion packages/browser/src/transports/base.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { API } from '@sentry/core';
import { Event, Response, Transport, TransportOptions } from '@sentry/types';
import { PromiseBuffer, SentryError } from '@sentry/utils';
import { parseRetryAfterHeader, PromiseBuffer, SentryError } from '@sentry/utils';

/** Base Transport class implementation */
export abstract class BaseTransport implements Transport {
Expand All @@ -15,6 +15,9 @@ export abstract class BaseTransport implements Transport {
/** A simple buffer holding all requests. */
protected readonly _buffer: PromiseBuffer<Response> = new PromiseBuffer(30);

/** Locks transport after receiving 429 response */
protected readonly _rateLimits: Record<string, Date> = {};

public constructor(public options: TransportOptions) {
this._api = new API(this.options.dsn);
// eslint-disable-next-line deprecation/deprecation
Expand All @@ -34,4 +37,45 @@ export abstract class BaseTransport implements Transport {
public close(timeout?: number): PromiseLike<boolean> {
return this._buffer.drain(timeout);
}

/**
* Gets the time that given category is disabled until for rate limiting
*/
protected _disabledUntil(category: string): Date {
return this._rateLimits[category] || this._rateLimits.all;
}

/**
* Checks if a category is ratelimited
*/
protected _isRateLimited(category: string): boolean {
// We use `new Date(Date.now())` instead of just `new Date()` despite them being the same thing,
// as it's easier to mock `now` method on `Date` instance instead of whole `Date` object in tests.
return this._disabledUntil(category) > new Date(Date.now());
}

/**
* Sets internal _rateLimits from incoming headers
*/
protected _handleRateLimit(headers: Record<string, string | null>): boolean {
const now = Date.now();
const rlHeader = headers['x-sentry-rate-limits'];
const raHeader = headers['retry-after'];

if (rlHeader) {
for (const limit of rlHeader.trim().split(',')) {
const parameters = limit.split(':', 2);
const headerDelay = parseInt(parameters[0], 10);
const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default
for (const category of parameters[1].split(';')) {
this._rateLimits[category || 'all'] = new Date(now + delay);
}
}
return true;
} else if (raHeader) {
this._rateLimits.all = new Date(now + parseRetryAfterHeader(now, raHeader));
return true;
}
return false;
}
}
35 changes: 19 additions & 16 deletions packages/browser/src/transports/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { eventToSentryRequest } from '@sentry/core';
import { Event, Response, Status } from '@sentry/types';
import { getGlobalObject, logger, parseRetryAfterHeader, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';
import { getGlobalObject, logger, supportsReferrerPolicy, SyncPromise } from '@sentry/utils';

import { BaseTransport } from './base';

const global = getGlobalObject<Window>();

/** `fetch` based transport */
export class FetchTransport extends BaseTransport {
/** Locks transport after receiving 429 response */
private _disabledUntil: Date = new Date(Date.now());

/**
* @inheritDoc
*/
public sendEvent(event: Event): PromiseLike<Response> {
if (new Date(Date.now()) < this._disabledUntil) {
const eventType = event.type || 'event';

if (this._isRateLimited(eventType)) {
return Promise.reject({
event,
reason: `Transport locked till ${this._disabledUntil} due to too many requests.`,
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
status: 429,
});
}
Expand Down Expand Up @@ -50,20 +49,24 @@ export class FetchTransport extends BaseTransport {
.then(response => {
const status = Status.fromHttpCode(response.status);

if (status === Status.Success) {
resolve({ status });
return;
}

if (status === Status.RateLimit) {
const now = Date.now();
// Request with 200 that contain `x-sentry-retry-limits` should still handle that header.
if (status === Status.Success || status === Status.RateLimit) {
/**
* "The name is case-insensitive."
* https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
*/
const retryAfterHeader = response.headers.get('Retry-After');
this._disabledUntil = new Date(now + parseRetryAfterHeader(now, retryAfterHeader));
logger.warn(`Too many requests, backing off till: ${this._disabledUntil}`);
const limited = this._handleRateLimit({
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
'retry-after': response.headers.get('Retry-After'),
});
if (limited) {
logger.warn(`Too many requests, backing off till: ${this._disabledUntil(eventType)}`);
}
}

if (status === Status.Success) {
resolve({ status });
return;
}

reject(response);
Expand Down
35 changes: 19 additions & 16 deletions packages/browser/src/transports/xhr.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { eventToSentryRequest } from '@sentry/core';
import { Event, Response, Status } from '@sentry/types';
import { logger, parseRetryAfterHeader, SyncPromise } from '@sentry/utils';
import { logger, SyncPromise } from '@sentry/utils';

import { BaseTransport } from './base';

/** `XHR` based transport */
export class XHRTransport extends BaseTransport {
/** Locks transport after receiving 429 response */
private _disabledUntil: Date = new Date(Date.now());

/**
* @inheritDoc
*/
public sendEvent(event: Event): PromiseLike<Response> {
if (new Date(Date.now()) < this._disabledUntil) {
const eventType = event.type || 'event';

if (this._isRateLimited(eventType)) {
return Promise.reject({
event,
reason: `Transport locked till ${this._disabledUntil} due to too many requests.`,
reason: `Transport locked till ${this._disabledUntil(eventType)} due to too many requests.`,
status: 429,
});
}
Expand All @@ -34,20 +33,24 @@ export class XHRTransport extends BaseTransport {

const status = Status.fromHttpCode(request.status);

if (status === Status.Success) {
resolve({ status });
return;
}

if (status === Status.RateLimit) {
const now = Date.now();
// Request with 200 that contain `x-sentry-retry-limits` should still handle that header.
if (status === Status.Success || status === Status.RateLimit) {
/**
* "The search for the header name is case-insensitive."
* https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getResponseHeader
*/
const retryAfterHeader = request.getResponseHeader('Retry-After');
this._disabledUntil = new Date(now + parseRetryAfterHeader(now, retryAfterHeader));
logger.warn(`Too many requests, backing off till: ${this._disabledUntil}`);
const limited = this._handleRateLimit({
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
'retry-after': request.getResponseHeader('Retry-After'),
});
if (limited) {
logger.warn(`Too many requests, backing off till: ${this._disabledUntil(eventType)}`);
}
}

if (status === Status.Success) {
resolve({ status });
return;
}

reject(request);
Expand Down
Loading