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.
@@ -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
- 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
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
- # Subscribe to Action Controller events
35
- subscribe_to_controller_events
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
- # Subscribe to Active Record events
38
- subscribe_to_active_record_events
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
- # Subscribe to Action View events
41
- subscribe_to_action_view_events
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
- # Subscribe to Action Mailer events (if available)
44
- subscribe_to_action_mailer_events if defined?(ActionMailer)
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
- # Subscribe to exception notifications
47
- subscribe_to_exception_notifications
283
+ Rails.logger.info "[ActiveRabbit] Middleware configured successfully"
48
284
  end
49
285
 
50
- initializer "active_rabbit.add_middleware" do |app|
51
- 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
52
292
 
53
- # Add request context middleware
54
- app.middleware.insert_before ActionDispatch::ShowExceptions, RequestContextMiddleware
293
+ initializer "active_rabbit.sidekiq" do
294
+ next unless defined?(Sidekiq)
55
295
 
56
- # Add exception catching middleware
57
- app.middleware.insert_before ActionDispatch::ShowExceptions, ExceptionMiddleware
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
- @app.call(env)
367
- rescue Exception => exception
368
- # 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
+
369
667
  begin
370
- 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
371
686
 
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
- }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRabbit
4
4
  module Client
5
- VERSION = "0.4.0"
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