activerabbit-ai 0.4.1 → 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.
@@ -12,6 +12,8 @@ rescue LoadError
12
12
  end
13
13
 
14
14
  require "securerandom"
15
+ require_relative "../reporting"
16
+ require_relative "../middleware/error_capture_middleware"
15
17
 
16
18
  module ActiveRabbit
17
19
  module Client
@@ -19,6 +21,13 @@ module ActiveRabbit
19
21
  config.active_rabbit = ActiveSupport::OrderedOptions.new
20
22
 
21
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
+
22
31
  # Configure ActiveRabbit from Rails configuration
23
32
  ActiveRabbit::Client.configure do |config|
24
33
  config.environment = Rails.env
@@ -26,37 +35,304 @@ module ActiveRabbit
26
35
  config.release = detect_release(app)
27
36
  end
28
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
+
29
45
  # Set up exception tracking
30
46
  setup_exception_tracking(app) if ActiveRabbit::Client.configured?
31
47
  end
32
48
 
33
- initializer "active_rabbit.subscribe_to_notifications" do
34
- next unless ActiveRabbit::Client.configured?
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
35
140
 
36
- # Subscribe to Action Controller events
37
- subscribe_to_controller_events
141
+ # Insert middleware in the correct order
142
+ puts "[ActiveRabbit] Inserting middleware..." if Rails.env.development?
38
143
 
39
- # Subscribe to Active Record events
40
- subscribe_to_active_record_events
144
+ # Insert ErrorCaptureMiddleware after ShowExceptions
145
+ app.config.middleware.insert_after(ActionDispatch::ShowExceptions, ActiveRabbit::Middleware::ErrorCaptureMiddleware)
41
146
 
42
- # Subscribe to Action View events
43
- subscribe_to_action_view_events
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)
44
150
 
45
- # Subscribe to Action Mailer events (if available)
46
- subscribe_to_action_mailer_events if defined?(ActionMailer)
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)
47
154
 
48
- # Subscribe to exception notifications
49
- subscribe_to_exception_notifications
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)
158
+
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
172
+
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
282
+
283
+ Rails.logger.info "[ActiveRabbit] Middleware configured successfully"
50
284
  end
51
285
 
52
- initializer "active_rabbit.add_middleware" do |app|
53
- next unless ActiveRabbit::Client.configured?
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
292
+
293
+ initializer "active_rabbit.sidekiq" do
294
+ next unless defined?(Sidekiq)
295
+
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
54
310
 
55
- # Add request context middleware
56
- app.middleware.insert_before ActionDispatch::ShowExceptions, RequestContextMiddleware
311
+ initializer "active_rabbit.active_job" do |app|
312
+ next unless defined?(ActiveJob)
57
313
 
58
- # Add exception catching middleware
59
- app.middleware.insert_before ActionDispatch::ShowExceptions, ExceptionMiddleware
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
60
336
  end
61
337
 
62
338
  initializer "active_rabbit.setup_shutdown_hooks" do
@@ -87,6 +363,20 @@ module ActiveRabbit
87
363
  end
88
364
  end
89
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
+
90
380
  private
91
381
 
92
382
  def setup_exception_tracking(app)
@@ -237,6 +527,7 @@ module ActiveRabbit
237
527
 
238
528
  ActiveRabbit::Client.track_exception(
239
529
  exception,
530
+ handled: true,
240
531
  context: {
241
532
  request: {
242
533
  method: data[:method],
@@ -365,41 +656,92 @@ module ActiveRabbit
365
656
  end
366
657
 
367
658
  def call(env)
368
- @app.call(env)
369
- rescue Exception => exception
370
- # Track the exception, but don't let tracking errors break the request
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
+
371
667
  begin
372
- request = ActionDispatch::Request.new(env)
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
373
686
 
374
- ActiveRabbit::Client.track_exception(
375
- exception,
376
- context: {
377
- request: {
378
- method: request.method,
379
- path: request.path,
380
- query_string: request.query_string,
381
- user_agent: request.headers["User-Agent"],
382
- ip_address: request.remote_ip,
383
- referer: request.referer,
384
- headers: sanitize_headers(request.headers)
385
- },
386
- middleware: {
387
- caught_by: 'ExceptionMiddleware',
388
- timestamp: Time.now.iso8601(3)
389
- }
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)
390
728
  }
391
- )
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)
392
737
  rescue => tracking_error
393
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 ")}"
394
741
  Rails.logger.error "[ActiveRabbit] Error tracking exception: #{tracking_error.message}" if defined?(Rails)
395
742
  end
396
-
397
- # Re-raise the original exception so Rails can handle it normally
398
- raise exception
399
743
  end
400
744
 
401
- private
402
-
403
745
  def sanitize_headers(headers)
404
746
  # Only include safe headers to avoid PII
405
747
  safe_headers = {}
@@ -414,5 +756,107 @@ module ActiveRabbit
414
756
  safe_headers
415
757
  end
416
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
417
861
  end
418
862
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRabbit
4
4
  module Client
5
- VERSION = "0.4.1"
5
+ VERSION = "0.4.2"
6
6
  end
7
7
  end
@@ -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
- exception_tracker.track_exception(
65
+ context_with_tags = context
66
+
67
+ # Track the exception
68
+ args = {
65
69
  exception: exception,
66
- context: 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