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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/check_api_data.rb +0 -0
- data/lib/active_rabbit/client/action_mailer_patch.rb +40 -0
- data/lib/active_rabbit/client/active_job_extensions.rb +66 -0
- data/lib/active_rabbit/client/configuration.rb +12 -3
- data/lib/active_rabbit/client/dedupe.rb +42 -0
- data/lib/active_rabbit/client/error_reporter.rb +78 -0
- data/lib/active_rabbit/client/event_processor.rb +5 -0
- data/lib/active_rabbit/client/exception_tracker.rb +90 -19
- data/lib/active_rabbit/client/http_client.rb +134 -16
- data/lib/active_rabbit/client/railtie.rb +492 -46
- data/lib/active_rabbit/client/version.rb +1 -1
- data/lib/active_rabbit/client.rb +23 -4
- data/lib/active_rabbit/middleware/error_capture_middleware.rb +24 -0
- data/lib/active_rabbit/reporting.rb +63 -0
- data/lib/active_rabbit/routing/not_found_app.rb +19 -0
- data/lib/active_rabbit-client.gemspec +0 -0
- data/lib/active_rabbit.rb +10 -2
- data/setup_local_gem_testing.sh +48 -0
- data/test_net_http.rb +47 -0
- data/test_with_api.rb +169 -0
- data/trigger_errors.rb +64 -0
- metadata +15 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
3
5
|
begin
|
|
4
6
|
require "rails/railtie"
|
|
5
7
|
rescue LoadError
|
|
@@ -10,51 +12,327 @@ rescue LoadError
|
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
require "securerandom"
|
|
15
|
+
require_relative "../reporting"
|
|
16
|
+
require_relative "../middleware/error_capture_middleware"
|
|
13
17
|
|
|
14
18
|
module ActiveRabbit
|
|
15
19
|
module Client
|
|
16
20
|
class Railtie < Rails::Railtie
|
|
17
21
|
config.active_rabbit = ActiveSupport::OrderedOptions.new
|
|
18
22
|
|
|
19
|
-
initializer "active_rabbit.configure" do |app|
|
|
23
|
+
initializer "active_rabbit.configure", after: :initialize_logger do |app|
|
|
24
|
+
if Rails.env.development?
|
|
25
|
+
puts "\n=== ActiveRabbit Configure ==="
|
|
26
|
+
puts "Environment: #{Rails.env}"
|
|
27
|
+
puts "Already configured? #{ActiveRabbit::Client.configured?}"
|
|
28
|
+
puts "================================\n"
|
|
29
|
+
end
|
|
30
|
+
|
|
20
31
|
# Configure ActiveRabbit from Rails configuration
|
|
21
32
|
ActiveRabbit::Client.configure do |config|
|
|
22
33
|
config.environment = Rails.env
|
|
23
|
-
config.logger = Rails.logger
|
|
34
|
+
config.logger = Rails.logger rescue Logger.new(STDOUT)
|
|
24
35
|
config.release = detect_release(app)
|
|
25
36
|
end
|
|
26
37
|
|
|
38
|
+
if Rails.env.development?
|
|
39
|
+
puts "\n=== ActiveRabbit Post-Configure ==="
|
|
40
|
+
puts "Now configured? #{ActiveRabbit::Client.configured?}"
|
|
41
|
+
puts "Configuration: #{ActiveRabbit::Client.configuration.inspect}"
|
|
42
|
+
puts "================================\n"
|
|
43
|
+
end
|
|
44
|
+
|
|
27
45
|
# Set up exception tracking
|
|
28
46
|
setup_exception_tracking(app) if ActiveRabbit::Client.configured?
|
|
29
47
|
end
|
|
30
48
|
|
|
31
|
-
initializer "active_rabbit.subscribe_to_notifications" do
|
|
32
|
-
|
|
49
|
+
initializer "active_rabbit.subscribe_to_notifications" do |app|
|
|
50
|
+
# Defer subscription until after application initializers (configuration complete)
|
|
51
|
+
app.config.after_initialize do
|
|
52
|
+
# Subscribe regardless; each handler guards on configured?
|
|
53
|
+
subscribe_to_controller_events
|
|
54
|
+
subscribe_to_active_record_events
|
|
55
|
+
subscribe_to_action_view_events
|
|
56
|
+
subscribe_to_action_mailer_events if defined?(ActionMailer)
|
|
57
|
+
subscribe_to_exception_notifications
|
|
58
|
+
|
|
59
|
+
# Fallback: low-level rack.exception subscription (older Rails and deep middleware errors)
|
|
60
|
+
ActiveSupport::Notifications.subscribe("rack.exception") do |*args|
|
|
61
|
+
begin
|
|
62
|
+
payload = args.last
|
|
63
|
+
exception = payload[:exception_object]
|
|
64
|
+
env = payload[:env]
|
|
65
|
+
next unless exception
|
|
66
|
+
|
|
67
|
+
ActiveRabbit::Reporting.report_exception(
|
|
68
|
+
exception,
|
|
69
|
+
env: env,
|
|
70
|
+
handled: false,
|
|
71
|
+
source: "rack.exception",
|
|
72
|
+
force: true
|
|
73
|
+
)
|
|
74
|
+
rescue => e
|
|
75
|
+
Rails.logger.error "[ActiveRabbit] Error handling rack.exception: #{e.class}: #{e.message}" if defined?(Rails)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Configure middleware after logger is initialized to avoid init cycles
|
|
82
|
+
initializer "active_rabbit.add_middleware", after: :initialize_logger do |app|
|
|
83
|
+
if Rails.env.development?
|
|
84
|
+
puts "\n=== ActiveRabbit Railtie Loading ==="
|
|
85
|
+
puts "Rails Environment: #{Rails.env}"
|
|
86
|
+
puts "Rails Middleware Stack Phase: #{app.middleware.respond_to?(:middlewares) ? 'Ready' : 'Not Ready'}"
|
|
87
|
+
puts "================================\n"
|
|
88
|
+
puts "\n=== Initial Middleware Stack ==="
|
|
89
|
+
puts "(not available at this boot phase)"
|
|
90
|
+
puts "=======================\n"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n=== Adding ActiveRabbit Middleware ===" if Rails.env.development?
|
|
94
|
+
# Handle both development (DebugExceptions) and production (ShowExceptions)
|
|
95
|
+
if defined?(ActionDispatch::DebugExceptions)
|
|
96
|
+
puts "[ActiveRabbit] Found DebugExceptions, configuring middleware..." if Rails.env.development?
|
|
97
|
+
|
|
98
|
+
# First remove any existing middleware to avoid duplicates
|
|
99
|
+
begin
|
|
100
|
+
app.config.middleware.delete(ActiveRabbit::Client::ExceptionMiddleware)
|
|
101
|
+
app.config.middleware.delete(ActiveRabbit::Client::RequestContextMiddleware)
|
|
102
|
+
app.config.middleware.delete(ActiveRabbit::Client::RoutingErrorCatcher)
|
|
103
|
+
puts "[ActiveRabbit] Cleaned up existing middleware" if Rails.env.development?
|
|
104
|
+
rescue => e
|
|
105
|
+
puts "[ActiveRabbit] Error cleaning middleware: #{e.message}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Insert middleware in the correct order
|
|
109
|
+
puts "[ActiveRabbit] Inserting middleware..." if Rails.env.development?
|
|
110
|
+
|
|
111
|
+
# Insert ErrorCaptureMiddleware after DebugExceptions to rely on rescue path
|
|
112
|
+
app.config.middleware.insert_after(ActionDispatch::DebugExceptions, ActiveRabbit::Middleware::ErrorCaptureMiddleware)
|
|
113
|
+
|
|
114
|
+
# Insert RequestContextMiddleware early in the stack
|
|
115
|
+
puts "[ActiveRabbit] Inserting RequestContextMiddleware before RequestId" if Rails.env.development?
|
|
116
|
+
app.config.middleware.insert_before(ActionDispatch::RequestId, ActiveRabbit::Client::RequestContextMiddleware)
|
|
117
|
+
|
|
118
|
+
# Insert ExceptionMiddleware before Rails' exception handlers (kept for env-based reporting)
|
|
119
|
+
puts "[ActiveRabbit] Inserting ExceptionMiddleware before DebugExceptions" if Rails.env.development?
|
|
120
|
+
app.config.middleware.insert_before(ActionDispatch::DebugExceptions, ActiveRabbit::Client::ExceptionMiddleware)
|
|
121
|
+
|
|
122
|
+
# Insert RoutingErrorCatcher after Rails' exception handlers
|
|
123
|
+
puts "[ActiveRabbit] Inserting RoutingErrorCatcher after DebugExceptions" if Rails.env.development?
|
|
124
|
+
app.config.middleware.insert_after(ActionDispatch::DebugExceptions, ActiveRabbit::Client::RoutingErrorCatcher)
|
|
125
|
+
|
|
126
|
+
puts "[ActiveRabbit] Middleware insertion complete" if Rails.env.development?
|
|
127
|
+
|
|
128
|
+
elsif defined?(ActionDispatch::ShowExceptions)
|
|
129
|
+
puts "[ActiveRabbit] Found ShowExceptions, configuring middleware..." if Rails.env.development?
|
|
130
|
+
|
|
131
|
+
# First remove any existing middleware to avoid duplicates
|
|
132
|
+
begin
|
|
133
|
+
app.config.middleware.delete(ActiveRabbit::Client::ExceptionMiddleware)
|
|
134
|
+
app.config.middleware.delete(ActiveRabbit::Client::RequestContextMiddleware)
|
|
135
|
+
app.config.middleware.delete(ActiveRabbit::Client::RoutingErrorCatcher)
|
|
136
|
+
puts "[ActiveRabbit] Cleaned up existing middleware" if Rails.env.development?
|
|
137
|
+
rescue => e
|
|
138
|
+
puts "[ActiveRabbit] Error cleaning middleware: #{e.message}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Insert middleware in the correct order
|
|
142
|
+
puts "[ActiveRabbit] Inserting middleware..." if Rails.env.development?
|
|
143
|
+
|
|
144
|
+
# Insert ErrorCaptureMiddleware after ShowExceptions
|
|
145
|
+
app.config.middleware.insert_after(ActionDispatch::ShowExceptions, ActiveRabbit::Middleware::ErrorCaptureMiddleware)
|
|
146
|
+
|
|
147
|
+
# Insert RequestContextMiddleware early in the stack
|
|
148
|
+
puts "[ActiveRabbit] Inserting RequestContextMiddleware before RequestId" if Rails.env.development?
|
|
149
|
+
app.config.middleware.insert_before(ActionDispatch::RequestId, ActiveRabbit::Client::RequestContextMiddleware)
|
|
33
150
|
|
|
34
|
-
|
|
35
|
-
|
|
151
|
+
# Insert ExceptionMiddleware before Rails' exception handlers
|
|
152
|
+
puts "[ActiveRabbit] Inserting ExceptionMiddleware before ShowExceptions" if Rails.env.development?
|
|
153
|
+
app.config.middleware.insert_before(ActionDispatch::ShowExceptions, ActiveRabbit::Client::ExceptionMiddleware)
|
|
36
154
|
|
|
37
|
-
|
|
38
|
-
|
|
155
|
+
# Insert RoutingErrorCatcher after Rails' exception handlers
|
|
156
|
+
puts "[ActiveRabbit] Inserting RoutingErrorCatcher after ShowExceptions" if Rails.env.development?
|
|
157
|
+
app.config.middleware.insert_after(ActionDispatch::ShowExceptions, ActiveRabbit::Client::RoutingErrorCatcher)
|
|
39
158
|
|
|
40
|
-
|
|
41
|
-
|
|
159
|
+
else
|
|
160
|
+
puts "[ActiveRabbit] No exception handlers found, using fallback configuration" if Rails.env.development?
|
|
161
|
+
app.config.middleware.use(ActiveRabbit::Middleware::ErrorCaptureMiddleware)
|
|
162
|
+
app.config.middleware.use(ActiveRabbit::Client::RequestContextMiddleware)
|
|
163
|
+
app.config.middleware.use(ActiveRabbit::Client::ExceptionMiddleware)
|
|
164
|
+
app.config.middleware.use(ActiveRabbit::Client::RoutingErrorCatcher)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
if Rails.env.development?
|
|
168
|
+
puts "\n=== Final Middleware Stack ==="
|
|
169
|
+
puts "(will be printed after initialize)"
|
|
170
|
+
puts "=======================\n"
|
|
171
|
+
end
|
|
42
172
|
|
|
43
|
-
|
|
44
|
-
|
|
173
|
+
# Add debug wrappers in development
|
|
174
|
+
if Rails.env.development?
|
|
175
|
+
# Wrap ExceptionMiddleware for detailed error tracking
|
|
176
|
+
ActiveRabbit::Client::ExceptionMiddleware.class_eval do
|
|
177
|
+
alias_method :__ar_original_call, :call unless method_defined?(:__ar_original_call)
|
|
178
|
+
def call(env)
|
|
179
|
+
puts "\n=== ExceptionMiddleware Enter ==="
|
|
180
|
+
puts "Path: #{env['PATH_INFO']}"
|
|
181
|
+
puts "Method: #{env['REQUEST_METHOD']}"
|
|
182
|
+
puts "Current Exception: #{env['action_dispatch.exception']&.class} - #{env['action_dispatch.exception']&.message}"
|
|
183
|
+
puts "Current Error: #{env['action_dispatch.error']&.class} - #{env['action_dispatch.error']&.message}"
|
|
184
|
+
puts "Rack Exception: #{env['rack.exception']&.class} - #{env['rack.exception']&.message}"
|
|
185
|
+
puts "Exception Backtrace: #{env['action_dispatch.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
186
|
+
puts "Error Backtrace: #{env['action_dispatch.error']&.backtrace&.first(3)&.join("\n ")}"
|
|
187
|
+
puts "Rack Backtrace: #{env['rack.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
188
|
+
puts "============================\n"
|
|
189
|
+
|
|
190
|
+
begin
|
|
191
|
+
status, headers, body = __ar_original_call(env)
|
|
192
|
+
puts "\n=== ExceptionMiddleware Exit (Success) ==="
|
|
193
|
+
puts "Status: #{status}"
|
|
194
|
+
puts "Headers: #{headers.inspect}"
|
|
195
|
+
puts "Final Exception: #{env['action_dispatch.exception']&.class} - #{env['action_dispatch.exception']&.message}"
|
|
196
|
+
puts "Final Error: #{env['action_dispatch.error']&.class} - #{env['action_dispatch.error']&.message}"
|
|
197
|
+
puts "Final Rack Exception: #{env['rack.exception']&.class} - #{env['rack.exception']&.message}"
|
|
198
|
+
puts "Final Exception Backtrace: #{env['action_dispatch.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
199
|
+
puts "Final Error Backtrace: #{env['action_dispatch.error']&.backtrace&.first(3)&.join("\n ")}"
|
|
200
|
+
puts "Final Rack Backtrace: #{env['rack.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
201
|
+
puts "===========================\n"
|
|
202
|
+
[status, headers, body]
|
|
203
|
+
rescue => e
|
|
204
|
+
puts "\n=== ExceptionMiddleware Exit (Error) ==="
|
|
205
|
+
puts "Error: #{e.class} - #{e.message}"
|
|
206
|
+
puts "Error Backtrace: #{e.backtrace&.first(3)&.join("\n ")}"
|
|
207
|
+
puts "Original Exception: #{env['action_dispatch.exception']&.class} - #{env['action_dispatch.exception']&.message}"
|
|
208
|
+
puts "Original Error: #{env['action_dispatch.error']&.class} - #{env['action_dispatch.error']&.message}"
|
|
209
|
+
puts "Original Rack Exception: #{env['rack.exception']&.class} - #{env['rack.exception']&.message}"
|
|
210
|
+
puts "Original Exception Backtrace: #{env['action_dispatch.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
211
|
+
puts "Original Error Backtrace: #{env['action_dispatch.error']&.backtrace&.first(3)&.join("\n ")}"
|
|
212
|
+
puts "Original Rack Backtrace: #{env['rack.exception']&.backtrace&.first(3)&.join("\n ")}"
|
|
213
|
+
puts "===========================\n"
|
|
214
|
+
raise
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Wrap RoutingErrorCatcher for detailed error tracking
|
|
220
|
+
ActiveRabbit::Client::RoutingErrorCatcher.class_eval do
|
|
221
|
+
alias_method :__ar_routing_original_call, :call unless method_defined?(:__ar_routing_original_call)
|
|
222
|
+
def call(env)
|
|
223
|
+
puts "\n=== RoutingErrorCatcher Enter ==="
|
|
224
|
+
puts "Path: #{env['PATH_INFO']}"
|
|
225
|
+
puts "Method: #{env['REQUEST_METHOD']}"
|
|
226
|
+
puts "Status: #{env['action_dispatch.exception']&.class}"
|
|
227
|
+
puts "============================\n"
|
|
228
|
+
|
|
229
|
+
begin
|
|
230
|
+
status, headers, body = __ar_routing_original_call(env)
|
|
231
|
+
puts "\n=== RoutingErrorCatcher Exit (Success) ==="
|
|
232
|
+
puts "Status: #{status}"
|
|
233
|
+
puts "===========================\n"
|
|
234
|
+
[status, headers, body]
|
|
235
|
+
rescue => e
|
|
236
|
+
puts "\n=== RoutingErrorCatcher Exit (Error) ==="
|
|
237
|
+
puts "Error: #{e.class} - #{e.message}"
|
|
238
|
+
puts "Backtrace: #{e.backtrace&.first(3)&.join("\n ")}"
|
|
239
|
+
puts "===========================\n"
|
|
240
|
+
raise
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# In development, add a hook to verify middleware after initialization
|
|
247
|
+
if Rails.env.development?
|
|
248
|
+
app.config.after_initialize do
|
|
249
|
+
Rails.logger.info "\n=== ActiveRabbit Configuration ==="
|
|
250
|
+
Rails.logger.info "Version: #{ActiveRabbit::Client::VERSION}"
|
|
251
|
+
Rails.logger.info "Environment: #{Rails.env}"
|
|
252
|
+
Rails.logger.info "API URL: #{ActiveRabbit::Client.configuration.api_url}"
|
|
253
|
+
Rails.logger.info "================================"
|
|
254
|
+
|
|
255
|
+
Rails.logger.info "\n=== Middleware Stack ==="
|
|
256
|
+
(Rails.application.middleware.middlewares rescue []).each do |mw|
|
|
257
|
+
klass = (mw.respond_to?(:klass) ? mw.klass.name : mw.to_s) rescue mw.inspect
|
|
258
|
+
Rails.logger.info " #{klass}"
|
|
259
|
+
end
|
|
260
|
+
Rails.logger.info "======================="
|
|
261
|
+
|
|
262
|
+
# Skip missing-middleware warnings in development since we may inject via alternate paths
|
|
263
|
+
unless Rails.env.development?
|
|
264
|
+
# Verify our middleware is present
|
|
265
|
+
our_middleware = [
|
|
266
|
+
ActiveRabbit::Client::ExceptionMiddleware,
|
|
267
|
+
ActiveRabbit::Client::RequestContextMiddleware,
|
|
268
|
+
ActiveRabbit::Client::RoutingErrorCatcher
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
stack_list = (Rails.application.middleware.middlewares rescue [])
|
|
272
|
+
missing = our_middleware.reject { |m| stack_list.any? { |x| (x.respond_to?(:klass) ? x.klass == m : false) } }
|
|
273
|
+
|
|
274
|
+
if missing.any?
|
|
275
|
+
Rails.logger.warn "\n⚠️ Missing ActiveRabbit middleware:"
|
|
276
|
+
missing.each { |m| Rails.logger.warn " - #{m}" }
|
|
277
|
+
Rails.logger.warn "This might affect error tracking!"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
45
282
|
|
|
46
|
-
|
|
47
|
-
subscribe_to_exception_notifications
|
|
283
|
+
Rails.logger.info "[ActiveRabbit] Middleware configured successfully"
|
|
48
284
|
end
|
|
49
285
|
|
|
50
|
-
initializer "active_rabbit.
|
|
51
|
-
|
|
286
|
+
initializer "active_rabbit.error_reporter" do |app|
|
|
287
|
+
# Defer attaching so application config has been applied
|
|
288
|
+
app.config.after_initialize do
|
|
289
|
+
ActiveRabbit::Client::ErrorReporter.attach!
|
|
290
|
+
end
|
|
291
|
+
end
|
|
52
292
|
|
|
53
|
-
|
|
54
|
-
|
|
293
|
+
initializer "active_rabbit.sidekiq" do
|
|
294
|
+
next unless defined?(Sidekiq)
|
|
55
295
|
|
|
56
|
-
#
|
|
57
|
-
|
|
296
|
+
# Report unhandled Sidekiq job errors
|
|
297
|
+
Sidekiq.configure_server do |config|
|
|
298
|
+
config.error_handlers << proc do |exception, context|
|
|
299
|
+
begin
|
|
300
|
+
ActiveRabbit::Client.track_exception(
|
|
301
|
+
exception,
|
|
302
|
+
context: { source: 'sidekiq', job: context }
|
|
303
|
+
)
|
|
304
|
+
rescue => e
|
|
305
|
+
Rails.logger.error "[ActiveRabbit] Sidekiq error handler failed: #{e.class} - #{e.message}" if defined?(Rails)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
initializer "active_rabbit.active_job" do |app|
|
|
312
|
+
next unless defined?(ActiveJob)
|
|
313
|
+
|
|
314
|
+
# Load extension
|
|
315
|
+
begin
|
|
316
|
+
require_relative "active_job_extensions"
|
|
317
|
+
rescue LoadError
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
app.config.after_initialize do
|
|
321
|
+
begin
|
|
322
|
+
ActiveJob::Base.include(ActiveRabbit::Client::ActiveJobExtensions)
|
|
323
|
+
rescue => e
|
|
324
|
+
Rails.logger.error "[ActiveRabbit] Failed to include ActiveJobExtensions: #{e.message}" if defined?(Rails)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
initializer "active_rabbit.action_mailer" do |app|
|
|
330
|
+
next unless defined?(ActionMailer)
|
|
331
|
+
|
|
332
|
+
begin
|
|
333
|
+
require_relative "action_mailer_patch"
|
|
334
|
+
rescue LoadError
|
|
335
|
+
end
|
|
58
336
|
end
|
|
59
337
|
|
|
60
338
|
initializer "active_rabbit.setup_shutdown_hooks" do
|
|
@@ -85,6 +363,20 @@ module ActiveRabbit
|
|
|
85
363
|
end
|
|
86
364
|
end
|
|
87
365
|
|
|
366
|
+
# Boot diagnostics to confirm wiring
|
|
367
|
+
initializer "active_rabbit.boot_diagnostics" do |app|
|
|
368
|
+
app.config.after_initialize do
|
|
369
|
+
begin
|
|
370
|
+
reporting_file, reporting_line = ActiveRabbit::Reporting.method(:report_exception).source_location
|
|
371
|
+
http_file, http_line = ActiveRabbit::Client::HttpClient.instance_method(:post_exception).source_location
|
|
372
|
+
Rails.logger.info "[ActiveRabbit] Reporting loaded from #{reporting_file}:#{reporting_line}" if defined?(Rails)
|
|
373
|
+
Rails.logger.info "[ActiveRabbit] HttpClient#post_exception from #{http_file}:#{http_line}" if defined?(Rails)
|
|
374
|
+
rescue => e
|
|
375
|
+
Rails.logger.debug "[ActiveRabbit] boot diagnostics failed: #{e.message}" if defined?(Rails)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
88
380
|
private
|
|
89
381
|
|
|
90
382
|
def setup_exception_tracking(app)
|
|
@@ -235,6 +527,7 @@ module ActiveRabbit
|
|
|
235
527
|
|
|
236
528
|
ActiveRabbit::Client.track_exception(
|
|
237
529
|
exception,
|
|
530
|
+
handled: true,
|
|
238
531
|
context: {
|
|
239
532
|
request: {
|
|
240
533
|
method: data[:method],
|
|
@@ -363,41 +656,92 @@ module ActiveRabbit
|
|
|
363
656
|
end
|
|
364
657
|
|
|
365
658
|
def call(env)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
659
|
+
# debug start - using Rails.logger to ensure it appears in development.log
|
|
660
|
+
Rails.logger.info "[AR] ExceptionMiddleware ENTER path=#{env['PATH_INFO']}" if defined?(Rails)
|
|
661
|
+
warn "[AR] ExceptionMiddleware ENTER path=#{env['PATH_INFO']}"
|
|
662
|
+
warn "[AR] Current exceptions in env:"
|
|
663
|
+
warn " - action_dispatch.exception: #{env['action_dispatch.exception']&.class}"
|
|
664
|
+
warn " - rack.exception: #{env['rack.exception']&.class}"
|
|
665
|
+
warn " - action_dispatch.error: #{env['action_dispatch.error']&.class}"
|
|
666
|
+
|
|
369
667
|
begin
|
|
370
|
-
|
|
668
|
+
# Try to call the app, catch any exceptions
|
|
669
|
+
status, headers, body = @app.call(env)
|
|
670
|
+
warn "[AR] App call completed with status: #{status}"
|
|
671
|
+
|
|
672
|
+
# Check for exceptions in env after app call
|
|
673
|
+
if (ex = env["action_dispatch.exception"] || env["rack.exception"] || env["action_dispatch.error"])
|
|
674
|
+
Rails.logger.info "[AR] env exception present: #{ex.class}: #{ex.message}" if defined?(Rails)
|
|
675
|
+
warn "[AR] env exception present: #{ex.class}: #{ex.message}"
|
|
676
|
+
warn "[AR] Exception backtrace: #{ex.backtrace&.first(3)&.join("\n ")}"
|
|
677
|
+
safe_report(ex, env, 'Rails rescued exception')
|
|
678
|
+
else
|
|
679
|
+
Rails.logger.info "[AR] env exception NOT present" if defined?(Rails)
|
|
680
|
+
warn "[AR] env exception NOT present"
|
|
681
|
+
warn "[AR] Final env check:"
|
|
682
|
+
warn " - action_dispatch.exception: #{env['action_dispatch.exception']&.class}"
|
|
683
|
+
warn " - rack.exception: #{env['rack.exception']&.class}"
|
|
684
|
+
warn " - action_dispatch.error: #{env['action_dispatch.error']&.class}"
|
|
685
|
+
end
|
|
371
686
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
687
|
+
# Return the response
|
|
688
|
+
[status, headers, body]
|
|
689
|
+
rescue => e
|
|
690
|
+
# Primary path: catch raw exceptions before Rails rescuers
|
|
691
|
+
Rails.logger.info "[AR] RESCUE caught: #{e.class}: #{e.message}" if defined?(Rails)
|
|
692
|
+
warn "[AR] RESCUE caught: #{e.class}: #{e.message}"
|
|
693
|
+
warn "[AR] Rescue backtrace: #{e.backtrace&.first(3)&.join("\n ")}"
|
|
694
|
+
|
|
695
|
+
# Report the exception
|
|
696
|
+
safe_report(e, env, 'Raw exception caught')
|
|
697
|
+
|
|
698
|
+
# Let Rails handle the exception
|
|
699
|
+
env["action_dispatch.exception"] = e
|
|
700
|
+
env["rack.exception"] = e
|
|
701
|
+
raise
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
private
|
|
706
|
+
|
|
707
|
+
def safe_report(exception, env, source)
|
|
708
|
+
begin
|
|
709
|
+
request = ActionDispatch::Request.new(env)
|
|
710
|
+
warn "[AR] safe_report called for #{source}"
|
|
711
|
+
warn "[AR] Exception: #{exception.class.name} - #{exception.message}"
|
|
712
|
+
warn "[AR] Backtrace: #{exception.backtrace&.first(3)&.join("\n ")}"
|
|
713
|
+
|
|
714
|
+
context = {
|
|
715
|
+
request: {
|
|
716
|
+
method: request.method,
|
|
717
|
+
path: request.path,
|
|
718
|
+
query_string: request.query_string,
|
|
719
|
+
user_agent: request.headers["User-Agent"],
|
|
720
|
+
ip_address: request.remote_ip,
|
|
721
|
+
referer: request.referer,
|
|
722
|
+
headers: sanitize_headers(request.headers)
|
|
723
|
+
},
|
|
724
|
+
middleware: {
|
|
725
|
+
caught_by: 'ExceptionMiddleware',
|
|
726
|
+
source: source,
|
|
727
|
+
timestamp: Time.now.iso8601(3)
|
|
388
728
|
}
|
|
389
|
-
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
warn "[AR] Tracking with context: #{context.inspect}"
|
|
732
|
+
|
|
733
|
+
result = ActiveRabbit::Client.track_exception(exception, context: context)
|
|
734
|
+
warn "[AR] Track result: #{result.inspect}"
|
|
735
|
+
|
|
736
|
+
Rails.logger.info "[ActiveRabbit] Tracked #{source}: #{exception.class.name} - #{exception.message}" if defined?(Rails)
|
|
390
737
|
rescue => tracking_error
|
|
391
738
|
# Log tracking errors but don't let them interfere with exception handling
|
|
739
|
+
warn "[AR] Error in safe_report: #{tracking_error.class} - #{tracking_error.message}"
|
|
740
|
+
warn "[AR] Error backtrace: #{tracking_error.backtrace&.first(3)&.join("\n ")}"
|
|
392
741
|
Rails.logger.error "[ActiveRabbit] Error tracking exception: #{tracking_error.message}" if defined?(Rails)
|
|
393
742
|
end
|
|
394
|
-
|
|
395
|
-
# Re-raise the original exception so Rails can handle it normally
|
|
396
|
-
raise exception
|
|
397
743
|
end
|
|
398
744
|
|
|
399
|
-
private
|
|
400
|
-
|
|
401
745
|
def sanitize_headers(headers)
|
|
402
746
|
# Only include safe headers to avoid PII
|
|
403
747
|
safe_headers = {}
|
|
@@ -412,5 +756,107 @@ module ActiveRabbit
|
|
|
412
756
|
safe_headers
|
|
413
757
|
end
|
|
414
758
|
end
|
|
759
|
+
|
|
760
|
+
# Middleware for catching routing errors
|
|
761
|
+
class RoutingErrorCatcher
|
|
762
|
+
def initialize(app) = @app = app
|
|
763
|
+
|
|
764
|
+
def call(env)
|
|
765
|
+
status, headers, body = @app.call(env)
|
|
766
|
+
|
|
767
|
+
if status == 404
|
|
768
|
+
Rails.logger.debug "[ActiveRabbit] RoutingErrorCatcher: 404 detected for #{env['PATH_INFO']}"
|
|
769
|
+
exception = env["action_dispatch.exception"] || env["rack.exception"] || env["action_dispatch.error"]
|
|
770
|
+
|
|
771
|
+
if exception && exception.is_a?(ActionController::RoutingError)
|
|
772
|
+
Rails.logger.debug "[ActiveRabbit] Routing error caught: #{exception.message}"
|
|
773
|
+
track_routing_error(exception, env)
|
|
774
|
+
else
|
|
775
|
+
# If no exception found in env, create one manually for 404s on non-asset paths
|
|
776
|
+
if env['PATH_INFO'] && !env['PATH_INFO'].match?(/\.(css|js|png|jpg|gif|ico|svg)$/)
|
|
777
|
+
synthetic_error = create_synthetic_error(env)
|
|
778
|
+
track_routing_error(synthetic_error, env)
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
[status, headers, body]
|
|
784
|
+
rescue => e
|
|
785
|
+
# Catch any routing errors that weren't handled by Rails
|
|
786
|
+
if e.is_a?(ActionController::RoutingError)
|
|
787
|
+
Rails.logger.debug "[ActiveRabbit] Unhandled routing error caught: #{e.message}"
|
|
788
|
+
track_routing_error(e, env, handled: false)
|
|
789
|
+
end
|
|
790
|
+
raise
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
private
|
|
794
|
+
|
|
795
|
+
def create_synthetic_error(env)
|
|
796
|
+
error = ActionController::RoutingError.new("No route matches [#{env['REQUEST_METHOD']}] \"#{env['PATH_INFO']}\"")
|
|
797
|
+
error.set_backtrace([
|
|
798
|
+
"#{Rails.root}/config/routes.rb:1:in `route_not_found'",
|
|
799
|
+
"#{__FILE__}:#{__LINE__}:in `call'",
|
|
800
|
+
"actionpack/lib/action_dispatch/middleware/debug_exceptions.rb:31:in `call'"
|
|
801
|
+
])
|
|
802
|
+
error
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
def track_routing_error(error, env, handled: true)
|
|
806
|
+
return unless defined?(ActiveRabbit::Client)
|
|
807
|
+
|
|
808
|
+
context = {
|
|
809
|
+
controller_action: 'Routing#not_found',
|
|
810
|
+
error_type: 'Route Not Found',
|
|
811
|
+
error_message: error.message,
|
|
812
|
+
error_location: error.backtrace&.first,
|
|
813
|
+
error_severity: :warning,
|
|
814
|
+
error_status: 404,
|
|
815
|
+
error_source: 'Router',
|
|
816
|
+
error_component: 'ActionDispatch',
|
|
817
|
+
error_action: 'route_lookup',
|
|
818
|
+
request_details: "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} (No Route)",
|
|
819
|
+
response_time: "N/A (Routing Error)",
|
|
820
|
+
routing_info: "No matching route for path: #{env['PATH_INFO']}",
|
|
821
|
+
environment: Rails.env,
|
|
822
|
+
occurred_at: Time.current.iso8601(3),
|
|
823
|
+
request_path: env['PATH_INFO'],
|
|
824
|
+
request_method: env['REQUEST_METHOD'],
|
|
825
|
+
handled: handled,
|
|
826
|
+
error: {
|
|
827
|
+
class: error.class.name,
|
|
828
|
+
message: error.message,
|
|
829
|
+
backtrace_preview: error.backtrace&.first(3),
|
|
830
|
+
handled: handled,
|
|
831
|
+
severity: :warning,
|
|
832
|
+
framework: 'Rails',
|
|
833
|
+
component: 'Router',
|
|
834
|
+
error_group: 'Routing Error',
|
|
835
|
+
error_type: 'route_not_found'
|
|
836
|
+
},
|
|
837
|
+
request: {
|
|
838
|
+
method: env['REQUEST_METHOD'],
|
|
839
|
+
path: env['PATH_INFO'],
|
|
840
|
+
query_string: env['QUERY_STRING'],
|
|
841
|
+
user_agent: env['HTTP_USER_AGENT'],
|
|
842
|
+
ip_address: env['REMOTE_ADDR']
|
|
843
|
+
},
|
|
844
|
+
routing: {
|
|
845
|
+
attempted_path: env['PATH_INFO'],
|
|
846
|
+
available_routes: 'See Rails routes',
|
|
847
|
+
error_type: 'route_not_found'
|
|
848
|
+
},
|
|
849
|
+
source: 'routing_error_catcher',
|
|
850
|
+
tags: {
|
|
851
|
+
error_type: 'routing_error',
|
|
852
|
+
handled: handled,
|
|
853
|
+
severity: 'warning'
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
# Force reporting so 404 ignore filters don't drop this
|
|
858
|
+
ActiveRabbit::Client.track_exception(error, context: context, handled: handled, force: true)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
415
861
|
end
|
|
416
862
|
end
|
data/lib/active_rabbit/client.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "client/exception_tracker"
|
|
|
8
8
|
require_relative "client/performance_monitor"
|
|
9
9
|
require_relative "client/n_plus_one_detector"
|
|
10
10
|
require_relative "client/pii_scrubber"
|
|
11
|
+
require_relative "client/error_reporter"
|
|
11
12
|
|
|
12
13
|
# Rails integration (optional)
|
|
13
14
|
begin
|
|
@@ -58,15 +59,28 @@ module ActiveRabbit
|
|
|
58
59
|
)
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def track_exception(exception, context: {}, user_id: nil, tags: {})
|
|
62
|
+
def track_exception(exception, context: {}, user_id: nil, tags: {}, handled: nil, force: false)
|
|
62
63
|
return unless configured?
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
context_with_tags = context
|
|
66
|
+
|
|
67
|
+
# Track the exception
|
|
68
|
+
args = {
|
|
65
69
|
exception: exception,
|
|
66
|
-
context:
|
|
70
|
+
context: context_with_tags,
|
|
67
71
|
user_id: user_id,
|
|
68
72
|
tags: tags
|
|
69
|
-
|
|
73
|
+
}
|
|
74
|
+
args[:handled] = handled unless handled.nil?
|
|
75
|
+
args[:force] = true if force
|
|
76
|
+
|
|
77
|
+
result = exception_tracker.track_exception(**args)
|
|
78
|
+
|
|
79
|
+
# Log the result
|
|
80
|
+
configuration.logger&.info("[ActiveRabbit] Exception tracked: #{exception.class.name}")
|
|
81
|
+
configuration.logger&.debug("[ActiveRabbit] Exception tracking result: #{result.inspect}")
|
|
82
|
+
|
|
83
|
+
result
|
|
70
84
|
end
|
|
71
85
|
|
|
72
86
|
def track_performance(name, duration_ms, metadata: {})
|
|
@@ -108,6 +122,11 @@ module ActiveRabbit
|
|
|
108
122
|
http_client.shutdown
|
|
109
123
|
end
|
|
110
124
|
|
|
125
|
+
# Manual capture convenience for non-Rails contexts
|
|
126
|
+
def capture_exception(exception, context: {}, user_id: nil, tags: {})
|
|
127
|
+
track_exception(exception, context: context, user_id: user_id, tags: tags)
|
|
128
|
+
end
|
|
129
|
+
|
|
111
130
|
private
|
|
112
131
|
|
|
113
132
|
def event_processor
|