1
1
import { API , SDK_VERSION } from '@sentry/core' ;
2
- import { DsnProtocol , Event , Response , SentryRequest , Status , Transport , TransportOptions } from '@sentry/types' ;
2
+ import {
3
+ DsnProtocol ,
4
+ Event ,
5
+ Response ,
6
+ SentryRequest ,
7
+ SentryRequestType ,
8
+ Session ,
9
+ Status ,
10
+ Transport ,
11
+ TransportOptions ,
12
+ } from '@sentry/types' ;
3
13
import { logger , parseRetryAfterHeader , PromiseBuffer , SentryError } from '@sentry/utils' ;
4
14
import * as fs from 'fs' ;
5
15
import * as http from 'http' ;
@@ -34,6 +44,14 @@ export interface HTTPModule {
34
44
// ): http.ClientRequest;
35
45
}
36
46
47
+ const CATEGORY_MAPPING : {
48
+ [ key in SentryRequestType ] : string ;
49
+ } = {
50
+ event : 'error' ,
51
+ transaction : 'transaction' ,
52
+ session : 'session' ,
53
+ } ;
54
+
37
55
/** Base Transport class implementation */
38
56
export abstract class BaseTransport implements Transport {
39
57
/** The Agent used for corresponding transport */
@@ -48,8 +66,8 @@ export abstract class BaseTransport implements Transport {
48
66
/** A simple buffer holding all requests. */
49
67
protected readonly _buffer : PromiseBuffer < Response > = new PromiseBuffer ( 30 ) ;
50
68
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 > = { } ;
53
71
54
72
/** Create instance and set this.dsn */
55
73
public constructor ( public options : TransportOptions ) {
@@ -123,13 +141,74 @@ export abstract class BaseTransport implements Transport {
123
141
} ;
124
142
}
125
143
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 ] && parameters [ 1 ] . split ( ';' ) ) || [ 'all' ] ) {
183
+ // categoriesAllowed is added here to ensure we are only storing rate limits for categories we support in this
184
+ // sdk and any categories that are not supported will not be added redundantly to the rateLimits object
185
+ const categoriesAllowed = [
186
+ ...( Object . keys ( CATEGORY_MAPPING ) as [ SentryRequestType ] ) . map ( k => CATEGORY_MAPPING [ k ] ) ,
187
+ 'all' ,
188
+ ] ;
189
+ if ( categoriesAllowed . includes ( category ) ) this . _rateLimits [ category ] = new Date ( now + delay ) ;
190
+ }
191
+ }
192
+ return true ;
193
+ } else if ( raHeader ) {
194
+ this . _rateLimits . all = new Date ( now + parseRetryAfterHeader ( now , raHeader ) ) ;
195
+ return true ;
196
+ }
197
+ return false ;
198
+ }
199
+
126
200
/** JSDoc */
127
- protected async _send ( sentryReq : SentryRequest ) : Promise < Response > {
201
+ protected async _send ( sentryReq : SentryRequest , originalPayload ?: Event | Session ) : Promise < Response > {
128
202
if ( ! this . module ) {
129
203
throw new SentryError ( 'No module available' ) ;
130
204
}
131
- if ( new Date ( Date . now ( ) ) < this . _disabledUntil ) {
132
- return Promise . reject ( new SentryError ( `Transport locked till ${ this . _disabledUntil } due to too many requests.` ) ) ;
205
+ if ( originalPayload && this . _isRateLimited ( sentryReq . type ) ) {
206
+ return Promise . reject ( {
207
+ payload : originalPayload ,
208
+ type : sentryReq . type ,
209
+ reason : `Transport locked till ${ this . _disabledUntil ( sentryReq . type ) } due to too many requests.` ,
210
+ status : 429 ,
211
+ } ) ;
133
212
}
134
213
135
214
if ( ! this . _buffer . isReady ( ) ) {
@@ -147,26 +226,31 @@ export abstract class BaseTransport implements Transport {
147
226
148
227
res . setEncoding ( 'utf8' ) ;
149
228
229
+ /**
230
+ * "Key-value pairs of header names and values. Header names are lower-cased."
231
+ * https://nodejs.org/api/http.html#http_message_headers
232
+ */
233
+ let retryAfterHeader = res . headers ? res . headers [ 'retry-after' ] : '' ;
234
+ retryAfterHeader = ( Array . isArray ( retryAfterHeader ) ? retryAfterHeader [ 0 ] : retryAfterHeader ) as string ;
235
+
236
+ let rlHeader = res . headers ? res . headers [ 'x-sentry-rate-limits' ] : '' ;
237
+ rlHeader = ( Array . isArray ( rlHeader ) ? rlHeader [ 0 ] : rlHeader ) as string ;
238
+
239
+ const headers = {
240
+ 'x-sentry-rate-limits' : rlHeader ,
241
+ 'retry-after' : retryAfterHeader ,
242
+ } ;
243
+
244
+ const limited = this . _handleRateLimit ( headers ) ;
245
+ if ( limited ) logger . warn ( `Too many requests, backing off until: ${ this . _disabledUntil ( sentryReq . type ) } ` ) ;
246
+
150
247
if ( status === Status . Success ) {
151
248
resolve ( { status } ) ;
152
249
} else {
153
- if ( status === Status . RateLimit ) {
154
- const now = Date . now ( ) ;
155
- /**
156
- * "Key-value pairs of header names and values. Header names are lower-cased."
157
- * https://nodejs.org/api/http.html#http_message_headers
158
- */
159
- let retryAfterHeader = res . headers ? res . headers [ 'retry-after' ] : '' ;
160
- retryAfterHeader = ( Array . isArray ( retryAfterHeader ) ? retryAfterHeader [ 0 ] : retryAfterHeader ) as string ;
161
- this . _disabledUntil = new Date ( now + parseRetryAfterHeader ( now , retryAfterHeader ) ) ;
162
- logger . warn ( `Too many requests, backing off till: ${ this . _disabledUntil } ` ) ;
163
- }
164
-
165
250
let rejectionMessage = `HTTP Error (${ statusCode } )` ;
166
251
if ( res . headers && res . headers [ 'x-sentry-error' ] ) {
167
252
rejectionMessage += `: ${ res . headers [ 'x-sentry-error' ] } ` ;
168
253
}
169
-
170
254
reject ( new SentryError ( rejectionMessage ) ) ;
171
255
}
172
256
0 commit comments