11import { 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' ;
44
55/** Base Transport class implementation */
66export abstract class BaseTransport implements Transport {
@@ -15,6 +15,9 @@ export abstract class BaseTransport implements Transport {
1515 /** A simple buffer holding all requests. */
1616 protected readonly _buffer : PromiseBuffer < Response > = new PromiseBuffer ( 30 ) ;
1717
18+ /** Locks transport after receiving rate limits in a response */
19+ protected readonly _rateLimits : Record < string , Date > = { } ;
20+
1821 public constructor ( public options : TransportOptions ) {
1922 this . _api = new API ( this . options . dsn ) ;
2023 // eslint-disable-next-line deprecation/deprecation
@@ -34,4 +37,75 @@ export abstract class BaseTransport implements Transport {
3437 public close ( timeout ?: number ) : PromiseLike < boolean > {
3538 return this . _buffer . drain ( timeout ) ;
3639 }
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+ }
37111}
0 commit comments