1
1
import { API } from '@sentry/core' ;
2
- import { Event , Response , Transport , TransportOptions } from '@sentry/types' ;
3
- import { PromiseBuffer , SentryError } from '@sentry/utils' ;
2
+ import { Event , Response , Status , Transport , TransportOptions } from '@sentry/types' ;
3
+ import { logger , parseRetryAfterHeader , PromiseBuffer , SentryError } from '@sentry/utils' ;
4
4
5
5
/** Base Transport class implementation */
6
6
export abstract class BaseTransport implements Transport {
@@ -15,6 +15,9 @@ export abstract class BaseTransport implements Transport {
15
15
/** A simple buffer holding all requests. */
16
16
protected readonly _buffer : PromiseBuffer < Response > = new PromiseBuffer ( 30 ) ;
17
17
18
+ /** Locks transport after receiving rate limits in a response */
19
+ protected readonly _rateLimits : Record < string , Date > = { } ;
20
+
18
21
public constructor ( public options : TransportOptions ) {
19
22
this . _api = new API ( this . options . dsn ) ;
20
23
// eslint-disable-next-line deprecation/deprecation
@@ -34,4 +37,75 @@ export abstract class BaseTransport implements Transport {
34
37
public close ( timeout ?: number ) : PromiseLike < boolean > {
35
38
return this . _buffer . drain ( timeout ) ;
36
39
}
40
+
41
+ /**
42
+ * Handle Sentry repsonse for promise-based transports.
43
+ */
44
+ protected _handleResponse ( {
45
+ eventType,
46
+ response,
47
+ headers,
48
+ resolve,
49
+ reject,
50
+ } : {
51
+ eventType : string ;
52
+ response : globalThis . Response | XMLHttpRequest ;
53
+ headers : Record < string , string | null > ;
54
+ resolve : ( value ?: Response | PromiseLike < Response > | null | undefined ) => void ;
55
+ reject : ( reason ?: unknown ) => void ;
56
+ } ) : void {
57
+ const status = Status . fromHttpCode ( response . status ) ;
58
+ /**
59
+ * "The name is case-insensitive."
60
+ * https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
61
+ */
62
+ const limited = this . _handleRateLimit ( headers ) ;
63
+ if ( limited ) logger . warn ( `Too many requests, backing off till: ${ this . _disabledUntil ( eventType ) } ` ) ;
64
+
65
+ if ( status === Status . Success ) {
66
+ resolve ( { status } ) ;
67
+ return ;
68
+ }
69
+
70
+ reject ( response ) ;
71
+ }
72
+
73
+ /**
74
+ * Gets the time that given category is disabled until for rate limiting
75
+ */
76
+ protected _disabledUntil ( category : string ) : Date {
77
+ return this . _rateLimits [ category ] || this . _rateLimits . all ;
78
+ }
79
+
80
+ /**
81
+ * Checks if a category is rate limited
82
+ */
83
+ protected _isRateLimited ( category : string ) : boolean {
84
+ return this . _disabledUntil ( category ) > new Date ( Date . now ( ) ) ;
85
+ }
86
+
87
+ /**
88
+ * Sets internal _rateLimits from incoming headers. Returns true if headers contains a non-empty rate limiting header.
89
+ */
90
+ protected _handleRateLimit ( headers : Record < string , string | null > ) : boolean {
91
+ const now = Date . now ( ) ;
92
+ const rlHeader = headers [ 'x-sentry-rate-limits' ] ;
93
+ const raHeader = headers [ 'retry-after' ] ;
94
+
95
+ if ( rlHeader ) {
96
+ for ( const limit of rlHeader . trim ( ) . split ( ',' ) ) {
97
+ const parameters = limit . split ( ':' , 2 ) ;
98
+ const headerDelay = parseInt ( parameters [ 0 ] , 10 ) ;
99
+ const delay = ( ! isNaN ( headerDelay ) ? headerDelay : 60 ) * 1000 ; // 60sec default
100
+ for ( const category of parameters [ 1 ] . split ( ';' ) ) {
101
+ this . _rateLimits [ category || 'all' ] = new Date ( now + delay ) ;
102
+ }
103
+ }
104
+ return true ;
105
+ } else if ( raHeader ) {
106
+ this . _rateLimits . all = new Date ( now + parseRetryAfterHeader ( now , raHeader ) ) ;
107
+ return true ;
108
+ }
109
+ return false ;
110
+ }
37
111
}
0 commit comments