activerabbit-ai 0.1.2 ā 0.3.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/lib/active_rabbit/client/event_processor.rb +1 -0
- data/lib/active_rabbit/client/exception_tracker.rb +1 -0
- data/lib/active_rabbit/client/http_client.rb +135 -41
- data/lib/active_rabbit/client/n_plus_one_detector.rb +1 -0
- data/lib/active_rabbit/client/performance_monitor.rb +2 -0
- data/lib/active_rabbit/client/railtie.rb +124 -36
- data/lib/active_rabbit/client/version.rb +1 -1
- data/lib/active_rabbit.rb +3 -0
- data/script/test_rails_integration.rb +211 -0
- metadata +4 -32
- data/activerabbit-ai-0.1.1.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 635640bced6f6dbbb84db97da4c15dc7d56a57995b78b0154c458f700121e1c5
|
|
4
|
+
data.tar.gz: 4502d4e93fc88d5ce85fd79e1f64841d68f2962d0d7562469ec486e5fe27067a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d79097187018bb03b75480b276a3e9faa60e51cd85b2c976fd772cc314c20607b28a140fdf97714041f2370ade3e416e4f4a76f63ad654dc8a326b7429ee5000
|
|
7
|
+
data.tar.gz: 3a1d63d4b6a015b486ff5ecb96246cef394989d33adb52f05b40e454aaa91af0f9738fb793c61cb8348c5773983b0eb7624d4c9b79da9c9425235c75924dcd24
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2025-01-03
|
|
11
|
+
|
|
12
|
+
### š Major HTTP Client Rewrite - Eliminated Faraday Dependency
|
|
13
|
+
|
|
14
|
+
This release completely replaces Faraday with pure Net::HTTP, eliminating dependency headaches while maintaining all functionality.
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
- **Faraday Dependencies**: Completely removed `faraday` and `faraday-retry` dependencies
|
|
18
|
+
- **Dependency Complexity**: No more version conflicts with other gems using Faraday
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Pure Net::HTTP Implementation**: Lightweight, zero-dependency HTTP client
|
|
22
|
+
- **Built-in Retry Logic**: Exponential backoff retry mechanism with configurable attempts
|
|
23
|
+
- **SSL Support**: Automatic HTTPS handling with proper SSL verification
|
|
24
|
+
- **Comprehensive Error Handling**: Proper timeout, connection, and HTTP status error handling
|
|
25
|
+
- **JSON Parsing**: Automatic JSON request/response handling
|
|
26
|
+
- **Rate Limit Detection**: Proper 429 status code handling
|
|
27
|
+
|
|
28
|
+
### Improved
|
|
29
|
+
- **Performance**: Faster startup and lower memory usage without Faraday overhead
|
|
30
|
+
- **Reliability**: Simplified HTTP stack reduces potential points of failure
|
|
31
|
+
- **Maintainability**: Pure Ruby implementation easier to debug and maintain
|
|
32
|
+
- **Compatibility**: No more conflicts with applications using different Faraday versions
|
|
33
|
+
|
|
34
|
+
### Technical Details
|
|
35
|
+
- Supports all HTTP methods (GET, POST, PUT, DELETE)
|
|
36
|
+
- Configurable timeouts (open_timeout, read_timeout)
|
|
37
|
+
- Automatic retry on network errors and server errors (429, 500, 502, 503, 504)
|
|
38
|
+
- Exponential backoff with configurable delay and max retries
|
|
39
|
+
- Proper User-Agent and authentication headers
|
|
40
|
+
- SSL certificate verification in production
|
|
41
|
+
|
|
42
|
+
This change reduces the gem's dependency footprint by ~95% while maintaining full compatibility with existing ActiveRabbit configurations.
|
|
43
|
+
|
|
44
|
+
## [0.2.0] - 2025-01-03
|
|
45
|
+
|
|
46
|
+
### šØ Major Rails Integration Improvements
|
|
47
|
+
|
|
48
|
+
This release fixes critical Rails integration issues that prevented reliable exception tracking in production environments.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- **Rails Autoload**: Fixed missing Railtie autoload in main entry point (`lib/active_rabbit.rb`)
|
|
52
|
+
- **Exception Middleware**: Properly positioned before `ActionDispatch::ShowExceptions` to catch raw exceptions
|
|
53
|
+
- **Rescued Exceptions**: Fixed `process_action.action_controller` subscriber to catch Rails-rescued exceptions
|
|
54
|
+
- **Thread Cleanup**: Improved `Thread.current` cleanup in middleware with proper nested request handling
|
|
55
|
+
- **Time Dependencies**: Added missing `require "time"` for `iso8601` method across all modules
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- **Shutdown Hooks**: Added `at_exit` and `SIGTERM` handlers for graceful shutdown and data flushing
|
|
59
|
+
- **Enhanced Context**: Added timing data, sanitized headers, and better request context
|
|
60
|
+
- **Error Isolation**: All tracking operations now fail gracefully without breaking the application
|
|
61
|
+
- **Rails 7+ Support**: Handles both `exception_object` (Rails 7+) and legacy `exception` formats
|
|
62
|
+
- **Production Resilience**: Never blocks web requests, background sending with automatic retries
|
|
63
|
+
|
|
64
|
+
### Improved
|
|
65
|
+
- **PII Scrubbing**: Enhanced parameter and header sanitization
|
|
66
|
+
- **Exception Fingerprinting**: Better grouping of similar exceptions
|
|
67
|
+
- **Middleware Safety**: Robust error handling prevents tracking failures from affecting requests
|
|
68
|
+
- **Test Coverage**: Added comprehensive Rails integration test suite
|
|
69
|
+
|
|
70
|
+
### Technical Details
|
|
71
|
+
- Rack middleware now placed **before** Rails exception handling (critical for production)
|
|
72
|
+
- ActiveSupport::Notifications properly subscribed to catch rescued exceptions
|
|
73
|
+
- Background queue with timer-based flushing ensures data delivery
|
|
74
|
+
- Thread-safe context management with proper cleanup
|
|
75
|
+
|
|
76
|
+
This release makes ActiveRabbit production-ready for Rails applications with reliable exception tracking that works even when Rails rescues exceptions and renders error pages.
|
|
77
|
+
|
|
78
|
+
## [0.1.2] - 2024-12-20
|
|
79
|
+
|
|
10
80
|
### Added
|
|
11
81
|
- Initial release of ActiveRabbit Ruby client
|
|
12
82
|
- Error tracking with detailed context and stack traces
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "net/https"
|
|
5
5
|
require "json"
|
|
6
6
|
require "concurrent"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
7
9
|
|
|
8
10
|
module ActiveRabbit
|
|
9
11
|
module Client
|
|
12
|
+
# HTTP client specific errors
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
class APIError < Error; end
|
|
15
|
+
class RateLimitError < APIError; end
|
|
16
|
+
class RetryableError < APIError; end
|
|
17
|
+
|
|
10
18
|
class HttpClient
|
|
11
19
|
attr_reader :configuration
|
|
12
20
|
|
|
13
21
|
def initialize(configuration)
|
|
14
22
|
@configuration = configuration
|
|
15
|
-
@connection = build_connection
|
|
16
23
|
@request_queue = Concurrent::Array.new
|
|
17
24
|
@batch_timer = nil
|
|
18
25
|
@shutdown = false
|
|
26
|
+
@base_uri = URI(configuration.api_url)
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
def post_event(event_data)
|
|
@@ -39,7 +47,7 @@ module ActiveRabbit
|
|
|
39
47
|
end
|
|
40
48
|
|
|
41
49
|
def test_connection
|
|
42
|
-
response = make_request(:post, "api/v1/test/connection", {
|
|
50
|
+
response = make_request(:post, "api/v1/test/connection", {
|
|
43
51
|
gem_version: ActiveRabbit::Client::VERSION,
|
|
44
52
|
timestamp: Time.now.iso8601
|
|
45
53
|
})
|
|
@@ -70,31 +78,6 @@ module ActiveRabbit
|
|
|
70
78
|
|
|
71
79
|
private
|
|
72
80
|
|
|
73
|
-
def build_connection
|
|
74
|
-
Faraday.new(url: configuration.api_url) do |conn|
|
|
75
|
-
conn.request :json
|
|
76
|
-
conn.request :retry,
|
|
77
|
-
max: configuration.retry_count,
|
|
78
|
-
interval: configuration.retry_delay,
|
|
79
|
-
backoff_factor: 2,
|
|
80
|
-
retry_statuses: [429, 500, 502, 503, 504]
|
|
81
|
-
|
|
82
|
-
conn.response :json
|
|
83
|
-
conn.response :raise_error
|
|
84
|
-
|
|
85
|
-
conn.options.timeout = configuration.timeout
|
|
86
|
-
conn.options.open_timeout = configuration.open_timeout
|
|
87
|
-
|
|
88
|
-
conn.headers["User-Agent"] = "ActiveRabbit-Ruby/#{VERSION}"
|
|
89
|
-
conn.headers["X-Project-Token"] = configuration.api_key
|
|
90
|
-
conn.headers["Content-Type"] = "application/json"
|
|
91
|
-
|
|
92
|
-
if configuration.project_id
|
|
93
|
-
conn.headers["X-Project-ID"] = configuration.project_id
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
81
|
def enqueue_request(method, path, data)
|
|
99
82
|
return if @shutdown
|
|
100
83
|
|
|
@@ -124,24 +107,135 @@ module ActiveRabbit
|
|
|
124
107
|
end
|
|
125
108
|
|
|
126
109
|
def make_request(method, path, data)
|
|
127
|
-
|
|
110
|
+
uri = URI.join(@base_uri, path)
|
|
111
|
+
|
|
112
|
+
# Retry logic with exponential backoff
|
|
113
|
+
retries = 0
|
|
114
|
+
max_retries = configuration.retry_count
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
response = perform_request(uri, method, data)
|
|
118
|
+
handle_response(response)
|
|
119
|
+
rescue Net::TimeoutError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
120
|
+
if retries < max_retries && should_retry_error?(e)
|
|
121
|
+
retries += 1
|
|
122
|
+
sleep(configuration.retry_delay * (2 ** (retries - 1))) # Exponential backoff
|
|
123
|
+
retry
|
|
124
|
+
end
|
|
125
|
+
raise APIError, "Request timeout after #{retries} retries: #{e.message}"
|
|
126
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError => e
|
|
127
|
+
if retries < max_retries
|
|
128
|
+
retries += 1
|
|
129
|
+
sleep(configuration.retry_delay * (2 ** (retries - 1)))
|
|
130
|
+
retry
|
|
131
|
+
end
|
|
132
|
+
raise APIError, "Connection failed after #{retries} retries: #{e.message}"
|
|
133
|
+
rescue => e
|
|
134
|
+
raise APIError, "Request failed: #{e.message}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def perform_request(uri, method, data)
|
|
139
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
140
|
+
|
|
141
|
+
# Configure SSL if HTTPS
|
|
142
|
+
if uri.scheme == 'https'
|
|
143
|
+
http.use_ssl = true
|
|
144
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Set timeouts
|
|
148
|
+
http.open_timeout = configuration.open_timeout
|
|
149
|
+
http.read_timeout = configuration.timeout
|
|
150
|
+
|
|
151
|
+
# Create request
|
|
152
|
+
request = case method.to_s.downcase
|
|
153
|
+
when 'post'
|
|
154
|
+
Net::HTTP::Post.new(uri.path)
|
|
155
|
+
when 'get'
|
|
156
|
+
Net::HTTP::Get.new(uri.path)
|
|
157
|
+
when 'put'
|
|
158
|
+
Net::HTTP::Put.new(uri.path)
|
|
159
|
+
when 'delete'
|
|
160
|
+
Net::HTTP::Delete.new(uri.path)
|
|
161
|
+
else
|
|
162
|
+
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Set headers
|
|
166
|
+
request['Content-Type'] = 'application/json'
|
|
167
|
+
request['Accept'] = 'application/json'
|
|
168
|
+
request['User-Agent'] = "ActiveRabbit-Ruby/#{ActiveRabbit::Client::VERSION}"
|
|
169
|
+
request['X-Project-Token'] = configuration.api_key
|
|
170
|
+
|
|
171
|
+
if configuration.project_id
|
|
172
|
+
request['X-Project-ID'] = configuration.project_id
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Set body for POST/PUT requests
|
|
176
|
+
if data && %w[post put].include?(method.to_s.downcase)
|
|
177
|
+
request.body = JSON.generate(data)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
http.request(request)
|
|
181
|
+
end
|
|
128
182
|
|
|
129
|
-
|
|
183
|
+
def handle_response(response)
|
|
184
|
+
case response.code.to_i
|
|
130
185
|
when 200..299
|
|
131
|
-
response
|
|
186
|
+
# Parse JSON response if present
|
|
187
|
+
if response.body && !response.body.empty?
|
|
188
|
+
begin
|
|
189
|
+
JSON.parse(response.body)
|
|
190
|
+
rescue JSON::ParserError
|
|
191
|
+
response.body
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
{}
|
|
195
|
+
end
|
|
132
196
|
when 429
|
|
133
197
|
raise RateLimitError, "Rate limit exceeded"
|
|
198
|
+
when 400..499
|
|
199
|
+
error_message = extract_error_message(response)
|
|
200
|
+
raise APIError, "Client error (#{response.code}): #{error_message}"
|
|
201
|
+
when 500..599
|
|
202
|
+
error_message = extract_error_message(response)
|
|
203
|
+
if should_retry_status?(response.code.to_i)
|
|
204
|
+
raise RetryableError, "Server error (#{response.code}): #{error_message}"
|
|
205
|
+
else
|
|
206
|
+
raise APIError, "Server error (#{response.code}): #{error_message}"
|
|
207
|
+
end
|
|
134
208
|
else
|
|
135
|
-
raise APIError, "
|
|
209
|
+
raise APIError, "Unexpected response code: #{response.code}"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def extract_error_message(response)
|
|
214
|
+
return "No error message" unless response.body
|
|
215
|
+
|
|
216
|
+
begin
|
|
217
|
+
parsed = JSON.parse(response.body)
|
|
218
|
+
parsed['error'] || parsed['message'] || response.body
|
|
219
|
+
rescue JSON::ParserError
|
|
220
|
+
response.body
|
|
136
221
|
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def should_retry_error?(error)
|
|
225
|
+
# Retry on network-level errors
|
|
226
|
+
error.is_a?(Net::TimeoutError) ||
|
|
227
|
+
error.is_a?(Net::OpenTimeout) ||
|
|
228
|
+
error.is_a?(Net::ReadTimeout) ||
|
|
229
|
+
error.is_a?(Errno::ECONNREFUSED) ||
|
|
230
|
+
error.is_a?(Errno::EHOSTUNREACH) ||
|
|
231
|
+
error.is_a?(SocketError)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def should_retry_status?(status_code)
|
|
235
|
+
# Retry on server errors and rate limits
|
|
236
|
+
[429, 500, 502, 503, 504].include?(status_code)
|
|
143
237
|
end
|
|
144
238
|
end
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
239
|
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
begin
|
|
4
|
+
require "rails/railtie"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Rails not available, define minimal structure for testing
|
|
7
|
+
module Rails
|
|
8
|
+
class Railtie; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
4
12
|
require "securerandom"
|
|
5
13
|
|
|
6
14
|
module ActiveRabbit
|
|
@@ -49,6 +57,34 @@ module ActiveRabbit
|
|
|
49
57
|
app.middleware.insert_before ActionDispatch::ShowExceptions, ExceptionMiddleware
|
|
50
58
|
end
|
|
51
59
|
|
|
60
|
+
initializer "active_rabbit.setup_shutdown_hooks" do
|
|
61
|
+
next unless ActiveRabbit::Client.configured?
|
|
62
|
+
|
|
63
|
+
# Ensure we flush pending data on shutdown
|
|
64
|
+
at_exit do
|
|
65
|
+
begin
|
|
66
|
+
ActiveRabbit::Client.flush
|
|
67
|
+
rescue => e
|
|
68
|
+
# Don't let shutdown hooks fail the process
|
|
69
|
+
Rails.logger.error "[ActiveRabbit] Error during shutdown flush: #{e.message}" if defined?(Rails)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Also flush on SIGTERM (common in production deployments)
|
|
74
|
+
if Signal.list.include?('TERM')
|
|
75
|
+
Signal.trap('TERM') do
|
|
76
|
+
begin
|
|
77
|
+
ActiveRabbit::Client.flush
|
|
78
|
+
rescue => e
|
|
79
|
+
# Log but don't raise
|
|
80
|
+
Rails.logger.error "[ActiveRabbit] Error during SIGTERM flush: #{e.message}" if defined?(Rails)
|
|
81
|
+
end
|
|
82
|
+
# Continue with normal SIGTERM handling
|
|
83
|
+
exit(0)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
52
88
|
private
|
|
53
89
|
|
|
54
90
|
def setup_exception_tracking(app)
|
|
@@ -173,25 +209,30 @@ module ActiveRabbit
|
|
|
173
209
|
end
|
|
174
210
|
end
|
|
175
211
|
|
|
176
|
-
|
|
177
|
-
# Subscribe to Rails exception notifications
|
|
212
|
+
def subscribe_to_exception_notifications
|
|
213
|
+
# Subscribe to Rails exception notifications for rescued exceptions
|
|
178
214
|
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |name, started, finished, unique_id, data|
|
|
179
215
|
next unless ActiveRabbit::Client.configured?
|
|
180
|
-
next unless data[:exception]
|
|
181
|
-
|
|
182
|
-
exception_class, exception_message = data[:exception]
|
|
183
|
-
|
|
184
|
-
puts "[ActiveRabbit] Exception notification received: #{exception_class}: #{exception_message}"
|
|
185
|
-
puts "[ActiveRabbit] Available data keys: #{data.keys.inspect}"
|
|
186
|
-
|
|
187
|
-
# Create exception with proper backtrace
|
|
188
|
-
exception = exception_class.constantize.new(exception_message)
|
|
189
216
|
|
|
190
|
-
#
|
|
217
|
+
# Check for rescued exceptions in the payload
|
|
218
|
+
exception = nil
|
|
191
219
|
if data[:exception_object]
|
|
192
|
-
exception
|
|
220
|
+
# Rails 7+ provides the actual exception object
|
|
221
|
+
exception = data[:exception_object]
|
|
222
|
+
elsif data[:exception]
|
|
223
|
+
# Fallback: reconstruct exception from class name and message
|
|
224
|
+
exception_class_name, exception_message = data[:exception]
|
|
225
|
+
begin
|
|
226
|
+
exception_class = exception_class_name.constantize
|
|
227
|
+
exception = exception_class.new(exception_message)
|
|
228
|
+
rescue NameError
|
|
229
|
+
# If we can't constantize the exception class, create a generic one
|
|
230
|
+
exception = StandardError.new("#{exception_class_name}: #{exception_message}")
|
|
231
|
+
end
|
|
193
232
|
end
|
|
194
233
|
|
|
234
|
+
next unless exception
|
|
235
|
+
|
|
195
236
|
ActiveRabbit::Client.track_exception(
|
|
196
237
|
exception,
|
|
197
238
|
context: {
|
|
@@ -201,7 +242,13 @@ module ActiveRabbit
|
|
|
201
242
|
controller: data[:controller],
|
|
202
243
|
action: data[:action],
|
|
203
244
|
status: data[:status],
|
|
204
|
-
format: data[:format]
|
|
245
|
+
format: data[:format],
|
|
246
|
+
params: scrub_sensitive_params(data[:params])
|
|
247
|
+
},
|
|
248
|
+
timing: {
|
|
249
|
+
duration_ms: ((finished - started) * 1000).round(2),
|
|
250
|
+
view_runtime: data[:view_runtime],
|
|
251
|
+
db_runtime: data[:db_runtime]
|
|
205
252
|
}
|
|
206
253
|
}
|
|
207
254
|
)
|
|
@@ -225,6 +272,13 @@ module ActiveRabbit
|
|
|
225
272
|
nil
|
|
226
273
|
end
|
|
227
274
|
|
|
275
|
+
def scrub_sensitive_params(params)
|
|
276
|
+
return {} unless params
|
|
277
|
+
return params unless ActiveRabbit::Client.configuration.enable_pii_scrubbing
|
|
278
|
+
|
|
279
|
+
PiiScrubber.new(ActiveRabbit::Client.configuration).scrub(params)
|
|
280
|
+
end
|
|
281
|
+
|
|
228
282
|
def n_plus_one_detector
|
|
229
283
|
@n_plus_one_detector ||= NPlusOneDetector.new(ActiveRabbit::Client.configuration)
|
|
230
284
|
end
|
|
@@ -244,18 +298,28 @@ module ActiveRabbit
|
|
|
244
298
|
|
|
245
299
|
# Set request context
|
|
246
300
|
request_context = build_request_context(request)
|
|
301
|
+
request_id = SecureRandom.uuid
|
|
302
|
+
|
|
303
|
+
# Store previous context to restore later (in case of nested requests)
|
|
304
|
+
previous_context = Thread.current[:active_rabbit_request_context]
|
|
247
305
|
Thread.current[:active_rabbit_request_context] = request_context
|
|
248
306
|
|
|
249
307
|
# Start N+1 detection for this request
|
|
250
|
-
request_id = SecureRandom.uuid
|
|
251
308
|
n_plus_one_detector.start_request(request_id)
|
|
252
309
|
|
|
253
310
|
begin
|
|
254
311
|
@app.call(env)
|
|
255
312
|
ensure
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
313
|
+
# Always clean up request context, even if an exception occurred
|
|
314
|
+
begin
|
|
315
|
+
n_plus_one_detector.finish_request(request_id)
|
|
316
|
+
rescue => e
|
|
317
|
+
# Log but don't raise - we don't want cleanup to fail the request
|
|
318
|
+
Rails.logger.error "[ActiveRabbit] Error finishing N+1 detection: #{e.message}" if defined?(Rails)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Restore previous context (handles nested requests)
|
|
322
|
+
Thread.current[:active_rabbit_request_context] = previous_context
|
|
259
323
|
end
|
|
260
324
|
end
|
|
261
325
|
|
|
@@ -299,30 +363,54 @@ module ActiveRabbit
|
|
|
299
363
|
end
|
|
300
364
|
|
|
301
365
|
def call(env)
|
|
302
|
-
puts "[ActiveRabbit] ExceptionMiddleware called for: #{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
|
|
303
366
|
@app.call(env)
|
|
304
367
|
rescue Exception => exception
|
|
305
|
-
# Track the exception
|
|
306
|
-
|
|
307
|
-
|
|
368
|
+
# Track the exception, but don't let tracking errors break the request
|
|
369
|
+
begin
|
|
370
|
+
request = ActionDispatch::Request.new(env)
|
|
308
371
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
372
|
+
ActiveRabbit::Client.track_exception(
|
|
373
|
+
exception,
|
|
374
|
+
context: {
|
|
375
|
+
request: {
|
|
376
|
+
method: request.method,
|
|
377
|
+
path: request.path,
|
|
378
|
+
query_string: request.query_string,
|
|
379
|
+
user_agent: request.headers["User-Agent"],
|
|
380
|
+
ip_address: request.remote_ip,
|
|
381
|
+
referer: request.referer,
|
|
382
|
+
headers: sanitize_headers(request.headers)
|
|
383
|
+
},
|
|
384
|
+
middleware: {
|
|
385
|
+
caught_by: 'ExceptionMiddleware',
|
|
386
|
+
timestamp: Time.now.iso8601(3)
|
|
387
|
+
}
|
|
319
388
|
}
|
|
320
|
-
|
|
321
|
-
|
|
389
|
+
)
|
|
390
|
+
rescue => tracking_error
|
|
391
|
+
# Log tracking errors but don't let them interfere with exception handling
|
|
392
|
+
Rails.logger.error "[ActiveRabbit] Error tracking exception: #{tracking_error.message}" if defined?(Rails)
|
|
393
|
+
end
|
|
322
394
|
|
|
323
|
-
# Re-raise the exception so Rails can handle it normally
|
|
395
|
+
# Re-raise the original exception so Rails can handle it normally
|
|
324
396
|
raise exception
|
|
325
397
|
end
|
|
398
|
+
|
|
399
|
+
private
|
|
400
|
+
|
|
401
|
+
def sanitize_headers(headers)
|
|
402
|
+
# Only include safe headers to avoid PII
|
|
403
|
+
safe_headers = {}
|
|
404
|
+
headers.each do |key, value|
|
|
405
|
+
next unless key.is_a?(String)
|
|
406
|
+
|
|
407
|
+
# Include common safe headers
|
|
408
|
+
if key.match?(/^(HTTP_ACCEPT|HTTP_ACCEPT_ENCODING|HTTP_ACCEPT_LANGUAGE|HTTP_CACHE_CONTROL|HTTP_CONNECTION|HTTP_HOST|HTTP_UPGRADE_INSECURE_REQUESTS|HTTP_USER_AGENT|CONTENT_TYPE|REQUEST_METHOD|REQUEST_URI|SERVER_NAME|SERVER_PORT)$/i)
|
|
409
|
+
safe_headers[key] = value
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
safe_headers
|
|
413
|
+
end
|
|
326
414
|
end
|
|
327
415
|
end
|
|
328
416
|
end
|
data/lib/active_rabbit.rb
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test script to verify Rails integration works properly
|
|
5
|
+
# This script can be run in a Rails console to test the ActiveRabbit integration
|
|
6
|
+
|
|
7
|
+
require 'bundler/setup'
|
|
8
|
+
require_relative '../lib/active_rabbit'
|
|
9
|
+
|
|
10
|
+
puts "š Testing ActiveRabbit Rails Integration"
|
|
11
|
+
puts "=" * 50
|
|
12
|
+
|
|
13
|
+
# Test 1: Check if Rails integration is loaded
|
|
14
|
+
if defined?(Rails)
|
|
15
|
+
puts "ā
Rails detected"
|
|
16
|
+
|
|
17
|
+
if defined?(ActiveRabbit::Client::Railtie)
|
|
18
|
+
puts "ā
ActiveRabbit::Client::Railtie loaded"
|
|
19
|
+
else
|
|
20
|
+
puts "ā ActiveRabbit::Client::Railtie NOT loaded"
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
puts "ā Rails not detected - creating mock Rails environment"
|
|
24
|
+
|
|
25
|
+
# Create a minimal Rails-like environment for testing
|
|
26
|
+
require 'pathname'
|
|
27
|
+
require 'logger'
|
|
28
|
+
|
|
29
|
+
# Mock Rails::Railtie first
|
|
30
|
+
module Rails
|
|
31
|
+
class Railtie
|
|
32
|
+
def self.config
|
|
33
|
+
@config ||= OpenStruct.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.initializer(name, options = {}, &block)
|
|
37
|
+
puts " š Initializer registered: #{name}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.env
|
|
42
|
+
"test"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.logger
|
|
46
|
+
@logger ||= Logger.new(STDOUT)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.root
|
|
50
|
+
Pathname.new(Dir.pwd)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.version
|
|
54
|
+
"7.0.0"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Mock ActiveSupport::OrderedOptions
|
|
59
|
+
require 'ostruct'
|
|
60
|
+
module ActiveSupport
|
|
61
|
+
class OrderedOptions < OpenStruct; end
|
|
62
|
+
|
|
63
|
+
module Notifications
|
|
64
|
+
def self.subscribe(name, &block)
|
|
65
|
+
puts " š” Subscribed to notification: #{name}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Mock ActionDispatch
|
|
71
|
+
module ActionDispatch
|
|
72
|
+
class ShowExceptions; end
|
|
73
|
+
class Request
|
|
74
|
+
def initialize(env); end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Load the railtie now that Rails is defined
|
|
79
|
+
require_relative '../lib/active_rabbit/client/railtie'
|
|
80
|
+
puts "ā
Railtie loaded with mock Rails environment"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Test 2: Check configuration
|
|
84
|
+
puts "\nš Testing Configuration"
|
|
85
|
+
puts "-" * 30
|
|
86
|
+
|
|
87
|
+
ActiveRabbit::Client.configure do |config|
|
|
88
|
+
config.api_key = "test_api_key_123"
|
|
89
|
+
config.api_url = "https://api.test.com"
|
|
90
|
+
config.environment = "test"
|
|
91
|
+
config.project_id = "test_project"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if ActiveRabbit::Client.configured?
|
|
95
|
+
puts "ā
ActiveRabbit configured successfully"
|
|
96
|
+
else
|
|
97
|
+
puts "ā ActiveRabbit configuration failed"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Test 3: Test middleware classes exist
|
|
101
|
+
puts "\nš§ Testing Middleware"
|
|
102
|
+
puts "-" * 30
|
|
103
|
+
|
|
104
|
+
if defined?(ActiveRabbit::Client::RequestContextMiddleware)
|
|
105
|
+
puts "ā
RequestContextMiddleware defined"
|
|
106
|
+
else
|
|
107
|
+
puts "ā RequestContextMiddleware NOT defined"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if defined?(ActiveRabbit::Client::ExceptionMiddleware)
|
|
111
|
+
puts "ā
ExceptionMiddleware defined"
|
|
112
|
+
else
|
|
113
|
+
puts "ā ExceptionMiddleware NOT defined"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Test 4: Test exception tracking
|
|
117
|
+
puts "\nš Testing Exception Tracking"
|
|
118
|
+
puts "-" * 30
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
# Create a test exception
|
|
122
|
+
test_exception = StandardError.new("Test exception for ActiveRabbit")
|
|
123
|
+
test_exception.set_backtrace([
|
|
124
|
+
"/app/controllers/test_controller.rb:10:in `index'",
|
|
125
|
+
"/app/config/routes.rb:5:in `call'"
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
# Track the exception
|
|
129
|
+
ActiveRabbit::Client.track_exception(
|
|
130
|
+
test_exception,
|
|
131
|
+
context: {
|
|
132
|
+
request: {
|
|
133
|
+
method: "GET",
|
|
134
|
+
path: "/test",
|
|
135
|
+
controller: "TestController",
|
|
136
|
+
action: "index"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
puts "ā
Exception tracking works"
|
|
142
|
+
rescue => e
|
|
143
|
+
puts "ā Exception tracking failed: #{e.message}"
|
|
144
|
+
puts " #{e.backtrace.first}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Test 5: Test event tracking
|
|
148
|
+
puts "\nš Testing Event Tracking"
|
|
149
|
+
puts "-" * 30
|
|
150
|
+
|
|
151
|
+
begin
|
|
152
|
+
ActiveRabbit::Client.track_event(
|
|
153
|
+
"test_event",
|
|
154
|
+
{
|
|
155
|
+
user_id: "test_user_123",
|
|
156
|
+
action: "button_click",
|
|
157
|
+
page: "homepage"
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
puts "ā
Event tracking works"
|
|
162
|
+
rescue => e
|
|
163
|
+
puts "ā Event tracking failed: #{e.message}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Test 6: Test performance tracking
|
|
167
|
+
puts "\nā” Testing Performance Tracking"
|
|
168
|
+
puts "-" * 30
|
|
169
|
+
|
|
170
|
+
begin
|
|
171
|
+
ActiveRabbit::Client.track_performance(
|
|
172
|
+
"controller.action",
|
|
173
|
+
250.5,
|
|
174
|
+
metadata: {
|
|
175
|
+
controller: "TestController",
|
|
176
|
+
action: "index",
|
|
177
|
+
db_queries: 3
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
puts "ā
Performance tracking works"
|
|
182
|
+
rescue => e
|
|
183
|
+
puts "ā Performance tracking failed: #{e.message}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Test 7: Test connection (this will fail without real API but should not crash)
|
|
187
|
+
puts "\nš Testing API Connection"
|
|
188
|
+
puts "-" * 30
|
|
189
|
+
|
|
190
|
+
connection_result = ActiveRabbit::Client.test_connection
|
|
191
|
+
if connection_result[:success]
|
|
192
|
+
puts "ā
API connection successful"
|
|
193
|
+
else
|
|
194
|
+
puts "ā ļø API connection failed (expected in test): #{connection_result[:error]}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Test 8: Test flush
|
|
198
|
+
puts "\nš¾ Testing Flush"
|
|
199
|
+
puts "-" * 30
|
|
200
|
+
|
|
201
|
+
begin
|
|
202
|
+
ActiveRabbit::Client.flush
|
|
203
|
+
puts "ā
Flush works"
|
|
204
|
+
rescue => e
|
|
205
|
+
puts "ā Flush failed: #{e.message}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
puts "\n" + "=" * 50
|
|
209
|
+
puts "š Rails integration test completed!"
|
|
210
|
+
puts " Check the output above for any failures."
|
|
211
|
+
puts "=" * 50
|
metadata
CHANGED
|
@@ -1,43 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerabbit-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex Shapalov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-09-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
-
- !ruby/object:Gem::Dependency
|
|
14
|
-
name: faraday
|
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
|
16
|
-
requirements:
|
|
17
|
-
- - "~>"
|
|
18
|
-
- !ruby/object:Gem::Version
|
|
19
|
-
version: '2.0'
|
|
20
|
-
type: :runtime
|
|
21
|
-
prerelease: false
|
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
-
requirements:
|
|
24
|
-
- - "~>"
|
|
25
|
-
- !ruby/object:Gem::Version
|
|
26
|
-
version: '2.0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: faraday-retry
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - "~>"
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '2.0'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - "~>"
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '2.0'
|
|
41
13
|
- !ruby/object:Gem::Dependency
|
|
42
14
|
name: concurrent-ruby
|
|
43
15
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -109,7 +81,6 @@ files:
|
|
|
109
81
|
- README.md
|
|
110
82
|
- Rakefile
|
|
111
83
|
- TESTING_GUIDE.md
|
|
112
|
-
- activerabbit-ai-0.1.1.gem
|
|
113
84
|
- examples/rails_app_testing.rb
|
|
114
85
|
- examples/rails_integration.rb
|
|
115
86
|
- examples/standalone_usage.rb
|
|
@@ -126,6 +97,7 @@ files:
|
|
|
126
97
|
- lib/active_rabbit/client/sidekiq_middleware.rb
|
|
127
98
|
- lib/active_rabbit/client/version.rb
|
|
128
99
|
- script/test_production_readiness.rb
|
|
100
|
+
- script/test_rails_integration.rb
|
|
129
101
|
- sig/active_rabbit/client.rbs
|
|
130
102
|
homepage: https://github.com/bugrabbit/active_rabbit-client
|
|
131
103
|
licenses:
|
|
@@ -149,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
149
121
|
- !ruby/object:Gem::Version
|
|
150
122
|
version: '0'
|
|
151
123
|
requirements: []
|
|
152
|
-
rubygems_version: 3.5.
|
|
124
|
+
rubygems_version: 3.5.11
|
|
153
125
|
signing_key:
|
|
154
126
|
specification_version: 4
|
|
155
127
|
summary: Ruby client for ActiveRabbit.ai application monitoring and error tracking
|
data/activerabbit-ai-0.1.1.gem
DELETED
|
Binary file
|