Class: Risenexa::Tracking::HttpClient
- Inherits:
-
Object
- Object
- Risenexa::Tracking::HttpClient
- Defined in:
- lib/risenexa/tracking/http_client.rb
Overview
HTTP layer for the Risenexa Tracking SDK.
Implements the retry algorithm from SDK-SPEC.md Section 4:
- Exponential backoff: BASE_DELAY * (MULTIPLIER ^ attempt), capped at MAX_DELAY
- Jitter: ±JITTER_FACTOR of the capped delay
- Retryable: 429, 500, 502, 503, timeout, connection errors
- Non-retryable: 401, 403, 404, 422 — raise immediately
- Idempotency: UUID v4 generated ONCE before first attempt, reused on all retries
Constant Summary collapse
- BASE_DELAY =
seconds
1.0- MULTIPLIER =
2.0- MAX_DELAY =
seconds
30.0- JITTER_FACTOR =
±20% of the capped delay
0.20- RETRYABLE_STATUSES =
[429, 500, 502, 503].freeze
- NON_RETRYABLE_STATUSES =
[401, 403, 404, 422].freeze
- TRACK_PATH =
"/api/v1/track"
Instance Method Summary collapse
-
#initialize(api_key:, base_url:, startup_slug:, timeout:, max_retries:) ⇒ HttpClient
constructor
A new instance of HttpClient.
-
#post(payload) ⇒ Result
POST the event payload to the Risenexa tracking endpoint.
Constructor Details
#initialize(api_key:, base_url:, startup_slug:, timeout:, max_retries:) ⇒ HttpClient
Returns a new instance of HttpClient.
29 30 31 32 33 34 35 |
# File 'lib/risenexa/tracking/http_client.rb', line 29 def initialize(api_key:, base_url:, startup_slug:, timeout:, max_retries:) @api_key = api_key @base_url = base_url @startup_slug = startup_slug @timeout = timeout @max_retries = max_retries end |
Instance Method Details
#post(payload) ⇒ Result
POST the event payload to the Risenexa tracking endpoint.
Generates a UUID v4 idempotency anchor before the first attempt and reuses it across all retry attempts.
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/risenexa/tracking/http_client.rb', line 51 def post(payload) # Use caller-provided event_id if present; otherwise generate a fresh UUID v4. # The event_id is generated ONCE before the first attempt and reused on all retries. event_id = payload[:event_id] || SecureRandom.uuid full_payload = payload.merge(event_id: event_id) attempt = 0 loop do begin response = perform_request(full_payload) rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError, Errno::EHOSTUNREACH => e if attempt >= @max_retries raise ConnectionError.new("Connection failed after #{attempt + 1} attempt(s): #{e.}", cause: e) end sleep(calculate_backoff(attempt)) attempt += 1 next end case response.code.to_i when 202 body = parse_json(response.body) return Result.new(status_code: 202, event_id: event_id, body: body) when *NON_RETRYABLE_STATUSES raise_non_retryable!(response) when *RETRYABLE_STATUSES if attempt >= @max_retries status = response.code.to_i if status == 429 raise RateLimitError.new( "Rate limit exceeded after #{attempt + 1} attempt(s)", retry_after: parse_retry_after(response) ) else raise MaxRetriesExceededError.new( "Max retries exceeded after #{attempt + 1} attempt(s)", last_response: response ) end end delay = if response.code.to_i == 429 parse_retry_after(response) || calculate_backoff(attempt) else calculate_backoff(attempt) end sleep(delay) attempt += 1 else raise Error, "Unexpected response status: #{response.code}" end end end |