1- import { API , eventToSentryRequest , SDK_VERSION } from '@sentry/core' ;
2- import { DsnProtocol , Event , Response , Status , Transport , TransportOptions } from '@sentry/types' ;
1+ import { API , SDK_VERSION } from '@sentry/core' ;
2+ import {
3+ DsnProtocol ,
4+ Event ,
5+ Response ,
6+ SentryRequest ,
7+ SentryRequestType ,
8+ Session ,
9+ Status ,
10+ Transport ,
11+ TransportOptions ,
12+ } from '@sentry/types' ;
313import { logger , parseRetryAfterHeader , PromiseBuffer , SentryError } from '@sentry/utils' ;
414import * as fs from 'fs' ;
515import * as http from 'http' ;
@@ -34,6 +44,14 @@ export interface HTTPModule {
3444 // ): http.ClientRequest;
3545}
3646
47+ const CATEGORY_MAPPING : {
48+ [ key in SentryRequestType ] : string ;
49+ } = {
50+ event : 'error' ,
51+ transaction : 'transaction' ,
52+ session : 'session' ,
53+ } ;
54+
3755/** Base Transport class implementation */
3856export abstract class BaseTransport implements Transport {
3957 /** The Agent used for corresponding transport */
@@ -48,8 +66,8 @@ export abstract class BaseTransport implements Transport {
4866 /** A simple buffer holding all requests. */
4967 protected readonly _buffer : PromiseBuffer < Response > = new PromiseBuffer ( 30 ) ;
5068
51- /** Locks transport after receiving 429 response */
52- private _disabledUntil : Date = new Date ( Date . now ( ) ) ;
69+ /** Locks transport after receiving rate limits in a response */
70+ protected readonly _rateLimits : Record < string , Date > = { } ;
5371
5472 /** Create instance and set this.dsn */
5573 public constructor ( public options : TransportOptions ) {
@@ -123,18 +141,76 @@ export abstract class BaseTransport implements Transport {
123141 } ;
124142 }
125143
144+ /**
145+ * Gets the time that given category is disabled until for rate limiting
146+ */
147+ protected _disabledUntil ( requestType : SentryRequestType ) : Date {
148+ const category = CATEGORY_MAPPING [ requestType ] ;
149+ return this . _rateLimits [ category ] || this . _rateLimits . all ;
150+ }
151+
152+ /**
153+ * Checks if a category is rate limited
154+ */
155+ protected _isRateLimited ( requestType : SentryRequestType ) : boolean {
156+ return this . _disabledUntil ( requestType ) > new Date ( Date . now ( ) ) ;
157+ }
158+
159+ /**
160+ * Sets internal _rateLimits from incoming headers. Returns true if headers contains a non-empty rate limiting header.
161+ */
162+ protected _handleRateLimit ( headers : Record < string , string | null > ) : boolean {
163+ const now = Date . now ( ) ;
164+ const rlHeader = headers [ 'x-sentry-rate-limits' ] ;
165+ const raHeader = headers [ 'retry-after' ] ;
166+
167+ if ( rlHeader ) {
168+ // rate limit headers are of the form
169+ // <header>,<header>,..
170+ // where each <header> is of the form
171+ // <retry_after>: <categories>: <scope>: <reason_code>
172+ // where
173+ // <retry_after> is a delay in ms
174+ // <categories> is the event type(s) (error, transaction, etc) being rate limited and is of the form
175+ // <category>;<category>;...
176+ // <scope> is what's being limited (org, project, or key) - ignored by SDK
177+ // <reason_code> is an arbitrary string like "org_quota" - ignored by SDK
178+ for ( const limit of rlHeader . trim ( ) . split ( ',' ) ) {
179+ const parameters = limit . split ( ':' , 2 ) ;
180+ const headerDelay = parseInt ( parameters [ 0 ] , 10 ) ;
181+ const delay = ( ! isNaN ( headerDelay ) ? headerDelay : 60 ) * 1000 ; // 60sec default
182+ for ( const category of parameters [ 1 ] . split ( ';' ) ) {
183+ this . _rateLimits [ category || 'all' ] = new Date ( now + delay ) ;
184+ }
185+ }
186+ return true ;
187+ } else if ( raHeader ) {
188+ this . _rateLimits . all = new Date ( now + parseRetryAfterHeader ( now , raHeader ) ) ;
189+ return true ;
190+ }
191+ return false ;
192+ }
193+
126194 /** JSDoc */
127- protected async _sendWithModule ( httpModule : HTTPModule , event : Event ) : Promise < Response > {
128- if ( new Date ( Date . now ( ) ) < this . _disabledUntil ) {
129- return Promise . reject ( new SentryError ( `Transport locked till ${ this . _disabledUntil } due to too many requests.` ) ) ;
195+ protected async _sendWithModule (
196+ httpModule : HTTPModule ,
197+ sentryReq : SentryRequest ,
198+ originalPayload ?: Event | Session ,
199+ ) : Promise < Response > {
200+ if ( originalPayload && this . _isRateLimited ( sentryReq . type ) ) {
201+ return Promise . reject ( {
202+ event : originalPayload ,
203+ type : sentryReq . type ,
204+ reason : `Transport locked till ${ this . _disabledUntil ( sentryReq . type ) } due to too many requests.` ,
205+ status : 429 ,
206+ } ) ;
130207 }
131208
132209 if ( ! this . _buffer . isReady ( ) ) {
133210 return Promise . reject ( new SentryError ( 'Not adding Promise due to buffer limit reached.' ) ) ;
134211 }
135212 return this . _buffer . add (
136213 new Promise < Response > ( ( resolve , reject ) => {
137- const sentryReq = eventToSentryRequest ( event , this . _api ) ;
138214 const options = this . _getRequestOptions ( new url . URL ( sentryReq . url ) ) ;
139215
140216 const req = httpModule . request ( options , ( res : http . IncomingMessage ) => {
@@ -143,29 +219,35 @@ export abstract class BaseTransport implements Transport {
143219
144220 res . setEncoding ( 'utf8' ) ;
145221
222+ /**
223+ * "Key-value pairs of header names and values. Header names are lower-cased."
224+ * https://nodejs.org/api/http.html#http_message_headers
225+ */
226+ let retryAfterHeader = res . headers ? res . headers [ 'retry-after' ] : '' ;
227+ retryAfterHeader = ( Array . isArray ( retryAfterHeader ) ? retryAfterHeader [ 0 ] : retryAfterHeader ) as string ;
228+
229+ let rlHeader = res . headers ? res . headers [ 'x-sentry-rate-limits' ] : '' ;
230+ rlHeader = ( Array . isArray ( rlHeader ) ? rlHeader [ 0 ] : rlHeader ) as string ;
231+
232+ const headers = {
233+ 'x-sentry-rate-limits' : rlHeader ,
234+ 'retry-after' : retryAfterHeader ,
235+ } ;
236+
237+ const limited = this . _handleRateLimit ( headers ) ;
238+ if ( limited ) logger . warn ( `Too many requests, backing off until: ${ this . _disabledUntil ( sentryReq . type ) } ` ) ;
239+
240+ let rejectionMessage = `HTTP Error (${ statusCode } )` ;
241+ if ( res . headers && res . headers [ 'x-sentry-error' ] ) {
242+ rejectionMessage += `: ${ res . headers [ 'x-sentry-error' ] } ` ;
243+ }
244+
146245 if ( status === Status . Success ) {
147246 resolve ( { status } ) ;
148- } else {
149- if ( status === Status . RateLimit ) {
150- const now = Date . now ( ) ;
151- /**
152- * "Key-value pairs of header names and values. Header names are lower-cased."
153- * https://nodejs.org/api/http.html#http_message_headers
154- */
155- let retryAfterHeader = res . headers ? res . headers [ 'retry-after' ] : '' ;
156- retryAfterHeader = ( Array . isArray ( retryAfterHeader ) ? retryAfterHeader [ 0 ] : retryAfterHeader ) as string ;
157- this . _disabledUntil = new Date ( now + parseRetryAfterHeader ( now , retryAfterHeader ) ) ;
158- logger . warn ( `Too many requests, backing off till: ${ this . _disabledUntil } ` ) ;
159- }
160-
161- let rejectionMessage = `HTTP Error (${ statusCode } )` ;
162- if ( res . headers && res . headers [ 'x-sentry-error' ] ) {
163- rejectionMessage += `: ${ res . headers [ 'x-sentry-error' ] } ` ;
164- }
165-
166- reject ( new SentryError ( rejectionMessage ) ) ;
167247 }
168248
249+ reject ( new SentryError ( rejectionMessage ) ) ;
250+
169251 // Force the socket to drain
170252 res . on ( 'data' , ( ) => {
171253 // Drain
0 commit comments