activerabbit-ai 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,9 +31,42 @@ module ActiveRabbit
31
31
  end
32
32
 
33
33
  def post_exception(exception_data)
34
- # Add event_type for batch processing
35
- exception_data_with_type = exception_data.merge(event_type: 'error')
36
- enqueue_request(:post, "api/v1/events/errors", exception_data_with_type)
34
+ # Sanitize data before any processing
35
+ exception_data_with_type = stringify_and_sanitize(exception_data.merge(event_type: 'error'))
36
+
37
+ # Primary endpoint path
38
+ path = "/api/v1/events/errors"
39
+
40
+ configuration.logger&.info("[ActiveRabbit] Sending exception to API...")
41
+ configuration.logger&.debug("[ActiveRabbit] Exception payload (pre-JSON): #{safe_preview(exception_data_with_type)}")
42
+ configuration.logger&.debug("[ActiveRabbit] Target path: #{path}")
43
+
44
+ begin
45
+ # Primary endpoint attempt
46
+ configuration.logger&.info("[ActiveRabbit] Making request to primary endpoint: POST #{path}")
47
+ response = make_request(:post, path, exception_data_with_type)
48
+ configuration.logger&.info("[ActiveRabbit] Exception sent successfully (errors endpoint)")
49
+ return response
50
+ rescue => e
51
+ configuration.logger&.error("[ActiveRabbit] Primary send failed: #{e.class}: #{e.message}")
52
+ configuration.logger&.error("[ActiveRabbit] Primary error backtrace: #{e.backtrace&.first(3)}")
53
+ configuration.logger&.debug("[ActiveRabbit] Falling back to /api/v1/events with type=error")
54
+
55
+ begin
56
+ # Fallback to generic events endpoint
57
+ fallback_path = "/api/v1/events"
58
+ fallback_body = { type: "error", data: exception_data_with_type }
59
+ configuration.logger&.info("[ActiveRabbit] Making request to fallback endpoint: POST #{fallback_path}")
60
+ response = make_request(:post, fallback_path, fallback_body)
61
+ configuration.logger&.info("[ActiveRabbit] Exception sent via fallback endpoint")
62
+ return response
63
+ rescue => e2
64
+ configuration.logger&.error("[ActiveRabbit] Fallback send failed: #{e2.class}: #{e2.message}")
65
+ configuration.logger&.error("[ActiveRabbit] Fallback error backtrace: #{e2.backtrace&.first(3)}")
66
+ configuration.logger&.error("[ActiveRabbit] All exception sending attempts failed")
67
+ nil
68
+ end
69
+ end
37
70
  end
38
71
 
39
72
  def post_performance(performance_data)
@@ -43,11 +76,24 @@ module ActiveRabbit
43
76
  end
44
77
 
45
78
  def post_batch(batch_data)
46
- make_request(:post, "api/v1/events/batch", { events: batch_data })
79
+ # Transform batch data into the format the API expects
80
+ events = batch_data.map do |event|
81
+ {
82
+ type: event[:data][:event_type] || event[:event_type] || event[:type],
83
+ data: event[:data]
84
+ }
85
+ end
86
+
87
+ # Send batch to API
88
+ configuration.logger&.info("[ActiveRabbit] Sending batch of #{events.length} events...")
89
+ response = make_request(:post, "/api/v1/events/batch", { events: events })
90
+ configuration.logger&.info("[ActiveRabbit] Batch sent successfully")
91
+ configuration.logger&.debug("[ActiveRabbit] Batch response: #{response.inspect}")
92
+ response
47
93
  end
48
94
 
49
95
  def test_connection
50
- response = make_request(:post, "api/v1/test/connection", {
96
+ response = make_request(:post, "/api/v1/test/connection", {
51
97
  gem_version: ActiveRabbit::Client::VERSION,
52
98
  timestamp: Time.now.iso8601
53
99
  })
@@ -59,13 +105,18 @@ module ActiveRabbit
59
105
  def flush
60
106
  return if @request_queue.empty?
61
107
 
108
+ configuration.logger&.info("[ActiveRabbit] Starting flush with #{@request_queue.length} items")
62
109
  batch = @request_queue.shift(@request_queue.length)
63
110
  return if batch.empty?
64
111
 
65
112
  begin
66
- post_batch(batch)
113
+ configuration.logger&.info("[ActiveRabbit] Sending batch of #{batch.length} items")
114
+ response = post_batch(batch)
115
+ configuration.logger&.info("[ActiveRabbit] Batch sent successfully: #{response.inspect}")
116
+ response
67
117
  rescue => e
68
118
  configuration.logger&.error("[ActiveRabbit] Failed to send batch: #{e.message}")
119
+ configuration.logger&.error("[ActiveRabbit] Backtrace: #{e.backtrace&.first(3)}")
69
120
  raise APIError, "Failed to send batch: #{e.message}"
70
121
  end
71
122
  end
@@ -81,13 +132,19 @@ module ActiveRabbit
81
132
  def enqueue_request(method, path, data)
82
133
  return if @shutdown
83
134
 
84
- @request_queue << {
135
+ # Format data for batch processing
136
+ formatted_data = {
85
137
  method: method,
86
138
  path: path,
87
139
  data: data,
88
140
  timestamp: Time.now.to_f
89
141
  }
90
142
 
143
+ configuration.logger&.info("[ActiveRabbit] Enqueueing request: #{method} #{path}")
144
+ configuration.logger&.debug("[ActiveRabbit] Request data: #{data.inspect}")
145
+
146
+ @request_queue << formatted_data
147
+
91
148
  # Start batch timer if not already running
92
149
  start_batch_timer if @batch_timer.nil? || @batch_timer.shutdown?
93
150
 
@@ -107,7 +164,14 @@ module ActiveRabbit
107
164
  end
108
165
 
109
166
  def make_request(method, path, data)
110
- uri = URI.join(@base_uri, path)
167
+ # Always rebuild base from current configuration to respect runtime changes
168
+ current_base = URI(configuration.api_url)
169
+ # Ensure path starts with a single leading slash
170
+ normalized_path = path.start_with?("/") ? path : "/#{path}"
171
+ uri = URI.join(current_base, normalized_path)
172
+ configuration.logger&.info("[ActiveRabbit] Making request: #{method.upcase} #{uri}")
173
+ configuration.logger&.debug("[ActiveRabbit] Request headers: X-Project-Token=#{configuration.api_key}, X-Project-ID=#{configuration.project_id}")
174
+ configuration.logger&.debug("[ActiveRabbit] Request body: #{safe_preview(data)}")
111
175
 
112
176
  # Retry logic with exponential backoff
113
177
  retries = 0
@@ -115,10 +179,17 @@ module ActiveRabbit
115
179
 
116
180
  begin
117
181
  response = perform_request(uri, method, data)
118
- handle_response(response)
182
+ configuration.logger&.info("[ActiveRabbit] Response status: #{response.code}")
183
+ configuration.logger&.debug("[ActiveRabbit] Response headers: #{response.to_hash.inspect}")
184
+ configuration.logger&.debug("[ActiveRabbit] Response body: #{response.body}")
185
+
186
+ result = handle_response(response)
187
+ configuration.logger&.debug("[ActiveRabbit] Parsed response: #{result.inspect}")
188
+ result
119
189
  rescue RetryableError => e
120
190
  if retries < max_retries
121
191
  retries += 1
192
+ configuration.logger&.info("[ActiveRabbit] Retrying request (#{retries}/#{max_retries})")
122
193
  sleep(configuration.retry_delay * (2 ** (retries - 1)))
123
194
  retry
124
195
  end
@@ -126,6 +197,7 @@ module ActiveRabbit
126
197
  rescue Net::OpenTimeout, Net::ReadTimeout => e
127
198
  if retries < max_retries && should_retry_error?(e)
128
199
  retries += 1
200
+ configuration.logger&.info("[ActiveRabbit] Retrying request after timeout (#{retries}/#{max_retries})")
129
201
  sleep(configuration.retry_delay * (2 ** (retries - 1))) # Exponential backoff
130
202
  retry
131
203
  end
@@ -133,19 +205,24 @@ module ActiveRabbit
133
205
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
134
206
  if retries < max_retries
135
207
  retries += 1
208
+ configuration.logger&.info("[ActiveRabbit] Retrying request after connection error (#{retries}/#{max_retries})")
136
209
  sleep(configuration.retry_delay * (2 ** (retries - 1)))
137
210
  retry
138
211
  end
139
212
  raise APIError, "Connection failed after #{retries} retries: #{e.message}"
140
213
  rescue APIError, RateLimitError => e
141
214
  # Re-raise API errors as-is
215
+ configuration.logger&.error("[ActiveRabbit] API error: #{e.class}: #{e.message}")
142
216
  raise e
143
217
  rescue => e
218
+ configuration.logger&.error("[ActiveRabbit] Request failed: #{e.class}: #{e.message}")
219
+ configuration.logger&.error("[ActiveRabbit] Backtrace: #{e.backtrace&.first(3)}")
144
220
  raise APIError, "Request failed: #{e.message}"
145
221
  end
146
222
  end
147
223
 
148
224
  def perform_request(uri, method, data)
225
+ configuration.logger&.debug("[ActiveRabbit] Making HTTP request: #{method.upcase} #{uri}")
149
226
  http = Net::HTTP.new(uri.host, uri.port)
150
227
 
151
228
  # Configure SSL if HTTPS
@@ -158,20 +235,22 @@ module ActiveRabbit
158
235
  http.open_timeout = configuration.open_timeout
159
236
  http.read_timeout = configuration.timeout
160
237
 
161
- # Create request
238
+ # Create request with full path
162
239
  request = case method.to_s.downcase
163
240
  when 'post'
164
- Net::HTTP::Post.new(uri.path)
241
+ Net::HTTP::Post.new(uri.request_uri)
165
242
  when 'get'
166
- Net::HTTP::Get.new(uri.path)
243
+ Net::HTTP::Get.new(uri.request_uri)
167
244
  when 'put'
168
- Net::HTTP::Put.new(uri.path)
245
+ Net::HTTP::Put.new(uri.request_uri)
169
246
  when 'delete'
170
- Net::HTTP::Delete.new(uri.path)
247
+ Net::HTTP::Delete.new(uri.request_uri)
171
248
  else
172
249
  raise ArgumentError, "Unsupported HTTP method: #{method}"
173
250
  end
174
251
 
252
+ configuration.logger&.debug("[ActiveRabbit] Request path: #{uri.request_uri}")
253
+
175
254
  # Set headers
176
255
  request['Content-Type'] = 'application/json'
177
256
  request['Accept'] = 'application/json'
@@ -191,31 +270,44 @@ module ActiveRabbit
191
270
  end
192
271
 
193
272
  def handle_response(response)
273
+ configuration.logger&.debug("[ActiveRabbit] Response code: #{response.code}")
274
+ configuration.logger&.debug("[ActiveRabbit] Response body: #{response.body}")
275
+
194
276
  case response.code.to_i
195
277
  when 200..299
196
278
  # Parse JSON response if present
197
279
  if response.body && !response.body.empty?
198
280
  begin
199
- JSON.parse(response.body)
200
- rescue JSON::ParserError
281
+ parsed = JSON.parse(response.body)
282
+ configuration.logger&.debug("[ActiveRabbit] Parsed response: #{parsed.inspect}")
283
+ parsed
284
+ rescue JSON::ParserError => e
285
+ configuration.logger&.error("[ActiveRabbit] Failed to parse response: #{e.message}")
286
+ configuration.logger&.error("[ActiveRabbit] Raw response: #{response.body}")
201
287
  response.body
202
288
  end
203
289
  else
290
+ configuration.logger&.debug("[ActiveRabbit] Empty response body")
204
291
  {}
205
292
  end
206
293
  when 429
294
+ configuration.logger&.error("[ActiveRabbit] Rate limit exceeded")
207
295
  raise RateLimitError, "Rate limit exceeded"
208
296
  when 400..499
209
297
  error_message = extract_error_message(response)
298
+ configuration.logger&.error("[ActiveRabbit] Client error (#{response.code}): #{error_message}")
210
299
  raise APIError, "Client error (#{response.code}): #{error_message}"
211
300
  when 500..599
212
301
  error_message = extract_error_message(response)
213
302
  if should_retry_status?(response.code.to_i)
303
+ configuration.logger&.warn("[ActiveRabbit] Retryable server error (#{response.code}): #{error_message}")
214
304
  raise RetryableError, "Server error (#{response.code}): #{error_message}"
215
305
  else
306
+ configuration.logger&.error("[ActiveRabbit] Server error (#{response.code}): #{error_message}")
216
307
  raise APIError, "Server error (#{response.code}): #{error_message}"
217
308
  end
218
309
  else
310
+ configuration.logger&.error("[ActiveRabbit] Unexpected response code: #{response.code}")
219
311
  raise APIError, "Unexpected response code: #{response.code}"
220
312
  end
221
313
  end
@@ -244,6 +336,32 @@ module ActiveRabbit
244
336
  # Retry on server errors and rate limits
245
337
  [429, 500, 502, 503, 504].include?(status_code)
246
338
  end
339
+
340
+ def stringify_and_sanitize(obj, depth: 0)
341
+ return nil if obj.nil?
342
+ return obj if obj.is_a?(Numeric) || obj.is_a?(TrueClass) || obj.is_a?(FalseClass)
343
+ return obj.to_s if obj.is_a?(Symbol) || obj.is_a?(Time) || obj.is_a?(Date) || obj.is_a?(URI) || obj.is_a?(Exception)
344
+
345
+ if obj.is_a?(Hash)
346
+ return obj.each_with_object({}) do |(k,v), h|
347
+ # limit depth to avoid accidental deep object graphs
348
+ h[k.to_s] = depth > 5 ? v.to_s : stringify_and_sanitize(v, depth: depth + 1)
349
+ end
350
+ end
351
+
352
+ if obj.is_a?(Array)
353
+ return obj.first(200).map { |v| depth > 5 ? v.to_s : stringify_and_sanitize(v, depth: depth + 1) }
354
+ end
355
+
356
+ # Fallback: best-effort string
357
+ obj.to_s
358
+ end
359
+
360
+ def safe_preview(obj)
361
+ # keep logs readable and safe
362
+ s = obj.inspect
363
+ s.length > 2000 ? s[0,2000] + "...(truncated)" : s
364
+ end
247
365
  end
248
366
 
249
367
  end