@@ -4,6 +4,7 @@ defmodule Sentry.Transport do
44 # This module is exclusively responsible for encoding and POSTing envelopes to Sentry.
55
66 alias Sentry . { ClientError , ClientReport , Config , Envelope , LoggerUtils }
7+ alias Sentry.Transport.RateLimiter
78
89 @ default_retries [ 1000 , 2000 , 4000 , 8000 ]
910 @ sentry_version 5
@@ -47,27 +48,13 @@ defmodule Sentry.Transport do
4748 retries_left ,
4849 envelope_items
4950 ) do
50- case request ( client , endpoint , headers , payload ) do
51+ case request ( client , endpoint , headers , payload , envelope_items ) do
5152 { :ok , id } ->
5253 { :ok , id }
5354
54- # If Sentry gives us a Retry-After header, we listen to that instead of our
55- # own retry.
56- { :retry_after , delay_ms } when retries_left != [ ] ->
57- Process . sleep ( delay_ms )
58-
59- post_envelope_with_retries (
60- client ,
61- endpoint ,
62- headers ,
63- payload ,
64- tl ( retries_left ) ,
65- envelope_items
66- )
67-
68- { :retry_after , _delay_ms } ->
55+ { :error , :rate_limited } ->
6956 ClientReport.Sender . record_discarded_events ( :ratelimit_backoff , envelope_items )
70- { :error , ClientError . new ( :too_many_retries ) }
57+ { :error , ClientError . new ( :rate_limited ) }
7158
7259 { :error , _reason } when retries_left != [ ] ->
7360 [ sleep_interval | retries_left ] = retries_left
@@ -92,29 +79,32 @@ defmodule Sentry.Transport do
9279 end
9380 end
9481
95- defp request ( client , endpoint , headers , body ) do
96- with { :ok , 200 , _headers , body } <-
82+ defp check_rate_limited ( envelope_items ) do
83+ rate_limited? =
84+ Enum . any? ( envelope_items , fn item ->
85+ category = Envelope . get_data_category ( item )
86+ RateLimiter . rate_limited? ( category )
87+ end )
88+
89+ if rate_limited? , do: { :error , :rate_limited } , else: :ok
90+ end
91+
92+ defp request ( client , endpoint , headers , body , envelope_items ) do
93+ with :ok <- check_rate_limited ( envelope_items ) ,
94+ { :ok , 200 , _headers , body } <-
9795 client_post_and_validate_return_value ( client , endpoint , headers , body ) ,
9896 { :ok , json } <- Sentry.JSON . decode ( body , Config . json_library ( ) ) do
9997 { :ok , Map . get ( json , "id" ) }
10098 else
101- { :ok , 429 , headers , _body } ->
102- delay_ms =
103- with timeout when is_binary ( timeout ) <-
104- :proplists . get_value ( "Retry-After" , headers , nil ) ,
105- { delay_s , "" } <- Integer . parse ( timeout ) do
106- delay_s * 1000
107- else
108- _ ->
109- # https://develop.sentry.dev/sdk/rate-limiting/#stage-1-parse-response-headers
110- 60_000
111- end
112-
113- { :retry_after , delay_ms }
99+ { :ok , 429 , _headers , _body } ->
100+ { :error , :rate_limited }
114101
115102 { :ok , status , headers , body } ->
116103 { :error , { :http , { status , headers , body } } }
117104
105+ { :error , :rate_limited } ->
106+ { :error , :rate_limited }
107+
118108 { :error , reason } ->
119109 { :error , { :request_failure , reason } }
120110 end
@@ -127,6 +117,7 @@ defmodule Sentry.Transport do
127117 { :ok , status , resp_headers , resp_body }
128118 when is_integer ( status ) and status in 200 .. 599 and is_list ( resp_headers ) and
129119 is_binary ( resp_body ) ->
120+ update_rate_limits ( resp_headers , status )
130121 { :ok , status , resp_headers , resp_body }
131122
132123 { :ok , status , resp_headers , resp_body } ->
@@ -137,6 +128,35 @@ defmodule Sentry.Transport do
137128 end
138129 end
139130
131+ defp update_rate_limits ( headers , status ) do
132+ rate_limits_header = :proplists . get_value ( "X-Sentry-Rate-Limits" , headers , nil )
133+
134+ cond do
135+ is_binary ( rate_limits_header ) ->
136+ # Use categorized rate limits if present
137+ RateLimiter . update_rate_limits ( rate_limits_header )
138+
139+ status == 429 ->
140+ # Use global rate limit from Retry-After if no categorized limits are present
141+ delay_seconds = get_global_delay ( headers )
142+ RateLimiter . update_global_rate_limit ( delay_seconds )
143+
144+ true ->
145+ :ok
146+ end
147+ end
148+
149+ defp get_global_delay ( headers ) do
150+ with timeout when is_binary ( timeout ) <- :proplists . get_value ( "Retry-After" , headers , nil ) ,
151+ { delay , "" } <- Integer . parse ( timeout ) do
152+ delay
153+ else
154+ # Per the spec, if Retry-After is missing or malformed, default to 60 seconds
155+ # https://develop.sentry.dev/sdk/rate-limiting/#stage-1-parse-response-headers
156+ _ -> 60
157+ end
158+ end
159+
140160 defp get_endpoint_and_headers do
141161 % Sentry.DSN { } = dsn = Config . dsn ( )
142162
0 commit comments