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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec5ef4721bc8a2096c534eb4f04e54d0520daa42dc95341feefa5bca17ba88e7
4
- data.tar.gz: bd3b5b9eb8879ecc3eb682126262e036a46dc3352be1f5b0ba71e7d234402868
3
+ metadata.gz: 48326a10836bf940aea7ebf3b6eca91f60086eb69e51e5e94266db5012d47ad9
4
+ data.tar.gz: 9244e314ba69d85fd9652604014520f8cbc6a4ab2c6da046b28a7f9c540a402e
5
5
  SHA512:
6
- metadata.gz: 6421b105cc63156671b119f8ce06fa94a5ce4fa5525f3f8eba96ac351b6da59f6cda1edb00acb03d607e9d7e9ca5074fb288cf6b3f8874468e67ad6ed5684d78
7
- data.tar.gz: 8543aa8f59de9a40a35936927fcb6010e62a45221ebc071ad513ebd7a6c3d2c42243d57a18f5e520d20d6045fefce14d1db78f567e68941cec9690267724e480
6
+ metadata.gz: 866f2f2f296c7a254526691e02e6c182aade30274985de6ca69f8e715b9af4036b489437eb2742840bf9fe1a582a0eaafae3ad0605342da4acde3dc6ee4d752a
7
+ data.tar.gz: f59a2b3dcfd170f048f96751f870cc238333fc576fbf200668ef040db9a88b97de8b1a0f60cf8c6490c3ee3f8c4ee1941318571d39d644921e0f04e2a2ae29c8
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.2] - 2025-01-04
6
+
7
+ ### Fixed
8
+ - **Major Rails 6.1 compatibility fix**: Added Rails Engine as primary integration method
9
+ - Engine loads after Rails initialization (safer than Railtie)
10
+ - Added comprehensive error handling for Rails loading edge cases
11
+ - Fallback mechanism: Engine -> Railtie -> Graceful degradation
12
+ - Fixed Logger initialization issues in Docker environments
13
+ - Added better error messages for debugging Rails integration issues
14
+
15
+ ### Added
16
+ - Rails Engine integration (`ActiveRabbit::Client::Engine`)
17
+ - Safer middleware insertion with error handling
18
+ - Enhanced shutdown hooks and signal handling
19
+ - Better request context management
20
+
21
+ ## [0.4.1] - 2025-01-04
22
+
23
+ ### Fixed
24
+ - Added explicit `require "logger"` to prevent Rails initialization issues
25
+ - Made Rails.logger access safer with fallback to STDOUT logger
26
+ - Changed initializer to run after `:initialize_logger` to ensure proper load order
27
+ - Fixed potential NameError with ActiveSupport::LoggerThreadSafeLevel::Logger
28
+
5
29
  ## [0.4.0] - 2025-01-04
6
30
 
7
31
  ### Added
data/check_api_data.rb ADDED
File without changes
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(ActionMailer)
4
+
5
+ module ActiveRabbit
6
+ module Client
7
+ module ActionMailerPatch
8
+ def deliver_now
9
+ start_time = Time.now
10
+ super
11
+ ensure
12
+ if ActiveRabbit::Client.configured?
13
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
14
+ ActiveRabbit::Client.track_event(
15
+ "email_sent",
16
+ {
17
+ mailer: self.class.name,
18
+ message_id: (message.message_id rescue nil),
19
+ subject: (message.subject rescue nil),
20
+ to: (Array(message.to).first rescue nil),
21
+ duration_ms: duration_ms
22
+ }
23
+ )
24
+ end
25
+ end
26
+
27
+ def deliver_later
28
+ ActiveRabbit::Client.track_event(
29
+ "email_enqueued",
30
+ { mailer: self.class.name, subject: (message.subject rescue nil), to: (Array(message.to).first rescue nil) }
31
+ ) if ActiveRabbit::Client.configured?
32
+ super
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ ActionMailer::MessageDelivery.prepend(ActiveRabbit::Client::ActionMailerPatch)
39
+
40
+
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRabbit
4
+ module Client
5
+ module ActiveJobExtensions
6
+ def self.included(base)
7
+ base.around_perform do |job, block|
8
+ start_time = Time.now
9
+
10
+ Thread.current[:active_rabbit_job_context] = {
11
+ job_class: job.class.name,
12
+ job_id: job.job_id,
13
+ queue_name: job.queue_name,
14
+ arguments: ActiveRabbit::Client::ActiveJobExtensions.scrub_arguments(job.arguments),
15
+ provider_job_id: (job.respond_to?(:provider_job_id) ? job.provider_job_id : nil)
16
+ }
17
+
18
+ begin
19
+ block.call
20
+
21
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
22
+ if ActiveRabbit::Client.configured?
23
+ ActiveRabbit::Client.track_performance(
24
+ "active_job.perform",
25
+ duration_ms,
26
+ metadata: { job_class: job.class.name, queue_name: job.queue_name, status: "completed" }
27
+ )
28
+ end
29
+ rescue Exception => exception
30
+ duration_ms = ((Time.now - start_time) * 1000).round(2)
31
+ if ActiveRabbit::Client.configured?
32
+ ActiveRabbit::Client.track_performance(
33
+ "active_job.perform",
34
+ duration_ms,
35
+ metadata: { job_class: job.class.name, queue_name: job.queue_name, status: "failed" }
36
+ )
37
+
38
+ ActiveRabbit::Client.track_exception(
39
+ exception,
40
+ context: {
41
+ job: {
42
+ job_class: job.class.name,
43
+ job_id: job.job_id,
44
+ queue_name: job.queue_name,
45
+ arguments: ActiveRabbit::Client::ActiveJobExtensions.scrub_arguments(job.arguments)
46
+ }
47
+ },
48
+ tags: { component: "active_job", queue: job.queue_name }
49
+ )
50
+ end
51
+ raise
52
+ ensure
53
+ Thread.current[:active_rabbit_job_context] = nil
54
+ end
55
+ end
56
+ end
57
+
58
+ def self.scrub_arguments(args)
59
+ return args unless ActiveRabbit::Client.configuration&.enable_pii_scrubbing
60
+ PiiScrubber.new(ActiveRabbit::Client.configuration).scrub(args)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+
@@ -11,12 +11,12 @@ module ActiveRabbit
11
11
  attr_accessor :batch_size, :flush_interval, :queue_size
12
12
  attr_accessor :enable_performance_monitoring, :enable_n_plus_one_detection
13
13
  attr_accessor :enable_pii_scrubbing, :pii_fields
14
- attr_accessor :ignored_exceptions, :ignored_user_agents
14
+ attr_accessor :ignored_exceptions, :ignored_user_agents, :ignore_404
15
15
  attr_accessor :release, :server_name, :logger
16
16
  attr_accessor :before_send_event, :before_send_exception
17
17
 
18
18
  def initialize
19
- @api_url = ENV.fetch("active_rabbit_API_URL", "https://api.activerabbit.com")
19
+ @api_url = ENV.fetch("active_rabbit_API_URL", "https://api.activerabbit.ai")
20
20
  @api_key = ENV["active_rabbit_API_KEY"]
21
21
  @project_id = ENV["active_rabbit_PROJECT_ID"]
22
22
  @environment = ENV.fetch("active_rabbit_ENVIRONMENT", detect_environment)
@@ -45,9 +45,10 @@ module ActiveRabbit
45
45
  ]
46
46
 
47
47
  # Filtering
48
+ # default ignores (404 controlled by ignore_404)
49
+ @ignore_404 = true
48
50
  @ignored_exceptions = %w[
49
51
  ActiveRecord::RecordNotFound
50
- ActionController::RoutingError
51
52
  ActionController::InvalidAuthenticityToken
52
53
  CGI::Session::CookieStore::TamperedWithCookie
53
54
  ]
@@ -83,6 +84,14 @@ module ActiveRabbit
83
84
 
84
85
  def should_ignore_exception?(exception)
85
86
  return false unless exception
87
+ # Special-case 404 via flag
88
+ if @ignore_404
89
+ begin
90
+ return true if exception.is_a?(ActionController::RoutingError)
91
+ rescue NameError
92
+ # Ignore if AC not loaded
93
+ end
94
+ end
86
95
 
87
96
  ignored_exceptions.any? do |ignored|
88
97
  case ignored
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module ActiveRabbit
6
+ module Client
7
+ module Dedupe
8
+ extend self
9
+
10
+ WINDOW_SECONDS = 5
11
+
12
+ @seen = {}
13
+ @lock = Monitor.new
14
+
15
+ def seen_recently?(exception, context = {}, window: WINDOW_SECONDS)
16
+ key = build_key(exception, context)
17
+ now = Time.now.to_f
18
+ @lock.synchronize do
19
+ prune!(now, window)
20
+ last = @seen[key]
21
+ @seen[key] = now
22
+ return last && (now - last) < window
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def prune!(now, window)
29
+ cutoff = now - window
30
+ @seen.delete_if { |_k, ts| ts < cutoff }
31
+ end
32
+
33
+ def build_key(exception, context)
34
+ top = Array(exception.backtrace).first.to_s
35
+ req_id = context[:request]&.[](:request_id) || context[:request_id] || context[:requestId]
36
+ [exception.class.name, top, req_id].join("|")
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+ require_relative '../reporting'
4
+
5
+ module ActiveRabbit
6
+ module Client
7
+ module ErrorReporter
8
+ class Subscriber
9
+ def report(exception, handled:, severity:, context:, source: nil)
10
+ begin
11
+ Rails.logger.info "[ActiveRabbit] Error reporter caught: #{exception.class}: #{exception.message}" if defined?(Rails.logger)
12
+
13
+ # Initialize de-dup set
14
+ $reported_errors ||= Set.new
15
+
16
+ # Generate a unique key for this error
17
+ error_key = "#{exception.class.name}:#{exception.message}:#{exception.backtrace&.first}"
18
+
19
+ # Only report if we haven't seen this error before and not deduped in short window
20
+ unless $reported_errors.include?(error_key)
21
+ $reported_errors.add(error_key)
22
+
23
+ dedupe_context = { request_id: (context && (context[:request_id] || context[:request]&.[](:request_id))) }
24
+ unless ActiveRabbit::Client::Dedupe.seen_recently?(exception, dedupe_context)
25
+ enriched = build_enriched_context(exception, handled: handled, severity: severity, context: context)
26
+ ActiveRabbit::Client.track_exception(exception, handled: handled, context: enriched)
27
+ end
28
+ end
29
+ rescue => e
30
+ Rails.logger.error "[ActiveRabbit] Error in ErrorReporter::Subscriber#report: #{e.class} - #{e.message}" if defined?(Rails.logger)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_enriched_context(exception, handled:, severity:, context: {})
37
+ ctx = { handled: handled, severity: severity, source: 'rails_error_reporter' }
38
+ ctx[:framework_context] = context || {}
39
+
40
+ env = context && (context[:env] || context['env'])
41
+ if env
42
+ req_info = ActiveRabbit::Reporting.rack_request_info(env)
43
+ ctx[:request] = req_info[:request]
44
+ ctx[:routing] = req_info[:routing]
45
+ # Top-level convenience for UI
46
+ ctx[:request_path] = ctx[:request][:path]
47
+ ctx[:request_method] = ctx[:request][:method]
48
+ end
49
+
50
+ if defined?(ActionController::RoutingError) && exception.is_a?(ActionController::RoutingError)
51
+ ctx[:controller_action] = 'Routing#not_found'
52
+ ctx[:error_type] = 'route_not_found'
53
+ ctx[:error_status] = 404
54
+ ctx[:error_component] = 'ActionDispatch'
55
+ ctx[:error_source] = 'Router'
56
+ ctx[:tags] = (ctx[:tags] || {}).merge(error_type: 'routing_error', severity: 'warning')
57
+ end
58
+
59
+ ctx
60
+ end
61
+ end
62
+
63
+ def self.attach!
64
+ # Rails 7.0+: Rails.error; earlier versions no-op
65
+ if defined?(Rails) && Rails.respond_to?(:error)
66
+ Rails.logger.info "[ActiveRabbit] Attaching to Rails error reporter" if defined?(Rails.logger)
67
+
68
+ subscriber = Subscriber.new
69
+ Rails.error.subscribe(subscriber)
70
+
71
+ Rails.logger.info "[ActiveRabbit] Rails error reporter attached successfully" if defined?(Rails.logger)
72
+ else
73
+ Rails.logger.info "[ActiveRabbit] Rails error reporter not available (Rails < 7.0)" if defined?(Rails.logger)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -102,6 +102,11 @@ module ActiveRabbit
102
102
  context[:request] = Thread.current[:active_rabbit_request_context]
103
103
  end
104
104
 
105
+ # Background job information (if available)
106
+ if defined?(Thread) && Thread.current[:active_rabbit_job_context]
107
+ context[:job] = Thread.current[:active_rabbit_job_context]
108
+ end
109
+
105
110
  context
106
111
  end
107
112
 
@@ -13,15 +13,16 @@ module ActiveRabbit
13
13
  @http_client = http_client
14
14
  end
15
15
 
16
- def track_exception(exception:, context: {}, user_id: nil, tags: {})
16
+ def track_exception(exception:, context: {}, user_id: nil, tags: {}, handled: nil, force: false)
17
17
  return unless exception
18
- return if should_ignore_exception?(exception)
18
+ return if !force && should_ignore_exception?(exception)
19
19
 
20
20
  exception_data = build_exception_data(
21
21
  exception: exception,
22
22
  context: context,
23
23
  user_id: user_id,
24
- tags: tags
24
+ tags: tags,
25
+ handled: handled
25
26
  )
26
27
 
27
28
  # Apply before_send callback if configured
@@ -30,7 +31,27 @@ module ActiveRabbit
30
31
  return unless exception_data # Callback can filter out exceptions by returning nil
31
32
  end
32
33
 
33
- http_client.post_exception(exception_data)
34
+ # Send exception to API and return response
35
+ configuration.logger&.info("[ActiveRabbit] Preparing to send exception: #{exception.class.name}")
36
+ configuration.logger&.debug("[ActiveRabbit] Exception data: #{exception_data.inspect}")
37
+
38
+ # Ensure we have required fields
39
+ unless exception_data[:exception_class] && exception_data[:message] && exception_data[:backtrace]
40
+ configuration.logger&.error("[ActiveRabbit] Missing required fields in exception data")
41
+ configuration.logger&.debug("[ActiveRabbit] Available fields: #{exception_data.keys.inspect}")
42
+ return nil
43
+ end
44
+
45
+ response = http_client.post_exception(exception_data)
46
+
47
+ if response.nil?
48
+ configuration.logger&.error("[ActiveRabbit] Failed to send exception - both primary and fallback endpoints failed")
49
+ return nil
50
+ end
51
+
52
+ configuration.logger&.info("[ActiveRabbit] Exception successfully sent to API")
53
+ configuration.logger&.debug("[ActiveRabbit] API Response: #{response.inspect}")
54
+ response
34
55
  end
35
56
 
36
57
  def flush
@@ -39,33 +60,83 @@ module ActiveRabbit
39
60
 
40
61
  private
41
62
 
42
- def build_exception_data(exception:, context:, user_id:, tags:)
43
- backtrace = parse_backtrace(exception.backtrace || [])
63
+ def build_exception_data(exception:, context:, user_id:, tags:, handled: nil)
64
+ parsed_bt = parse_backtrace(exception.backtrace || [])
65
+ backtrace_lines = parsed_bt.map { |frame| frame[:line] }
66
+
67
+ # Fallback: synthesize a helpful frame for routing errors with no backtrace
68
+ if backtrace_lines.empty?
69
+ synthetic = nil
70
+ if context && (context[:routing]&.[](:path) || context[:request_path])
71
+ path = context[:routing]&.[](:path) || context[:request_path]
72
+ synthetic = "#{defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : 'app'}/config/routes.rb:1:in `route_not_found' for #{path}"
73
+ elsif exception && exception.message && exception.message =~ /No route matches \[(\w+)\] \"(.+?)\"/
74
+ path = $2
75
+ synthetic = "#{defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : 'app'}/config/routes.rb:1:in `route_not_found' for #{path}"
76
+ end
77
+ backtrace_lines = [synthetic] if synthetic
78
+ end
44
79
 
80
+ # Build data in the format the API expects
45
81
  data = {
46
- type: exception.class.name,
82
+ # Required fields
83
+ exception_class: exception.class.name,
47
84
  message: exception.message,
48
- backtrace: backtrace,
49
- fingerprint: generate_fingerprint(exception),
50
- timestamp: Time.now.iso8601(3),
51
- environment: configuration.environment,
52
- release: configuration.release,
85
+ backtrace: backtrace_lines,
86
+
87
+ # Timing and environment
88
+ occurred_at: Time.now.iso8601(3),
89
+ environment: configuration.environment || 'development',
90
+ release_version: configuration.release,
53
91
  server_name: configuration.server_name,
54
- context: scrub_pii(context || {}),
55
- tags: tags || {}
56
- }
57
92
 
58
- data[:user_id] = user_id if user_id
59
- data[:project_id] = configuration.project_id if configuration.project_id
93
+ # Context from the error
94
+ controller_action: context[:controller_action],
95
+ request_path: context[:request_path],
96
+ request_method: context[:request_method],
60
97
 
61
- # Add runtime context
62
- data[:runtime_context] = build_runtime_context
98
+ # Additional context
99
+ context: scrub_pii(context || {}),
100
+ tags: tags || {},
101
+ user_id: user_id,
102
+ project_id: configuration.project_id,
103
+
104
+ # Runtime info
105
+ runtime_context: build_runtime_context,
106
+
107
+ # Error details (for better UI display)
108
+ error_type: context[:error_type] || exception.class.name,
109
+ error_message: context[:error_message] || exception.message,
110
+ error_location: context[:error_location] || backtrace_lines.first,
111
+ error_severity: context[:error_severity] || :error,
112
+ error_status: context[:error_status] || 500,
113
+ error_source: context[:error_source] || 'Application',
114
+ error_component: context[:error_component] || 'Unknown',
115
+ error_action: context[:error_action],
116
+ handled: context.key?(:handled) ? context[:handled] : handled,
117
+
118
+ # Request details
119
+ request_details: context[:request_details],
120
+ response_time: context[:response_time],
121
+ routing_info: context[:routing_info]
122
+ }
63
123
 
64
124
  # Add request context if available
65
125
  if defined?(Thread) && Thread.current[:active_rabbit_request_context]
66
126
  data[:request_context] = Thread.current[:active_rabbit_request_context]
67
127
  end
68
128
 
129
+ # Add background job context if available
130
+ if defined?(Thread) && Thread.current[:active_rabbit_job_context]
131
+ data[:job_context] = Thread.current[:active_rabbit_job_context]
132
+ end
133
+
134
+ # Log what we're sending
135
+ configuration.logger&.debug("[ActiveRabbit] Built exception data:")
136
+ configuration.logger&.debug("[ActiveRabbit] - Required fields: class=#{data[:exception_class]}, message=#{data[:message]}, backtrace=#{data[:backtrace]&.first}")
137
+ configuration.logger&.debug("[ActiveRabbit] - Error details: type=#{data[:error_type]}, source=#{data[:error_source]}, component=#{data[:error_component]}")
138
+ configuration.logger&.debug("[ActiveRabbit] - Request info: path=#{data[:request_path]}, method=#{data[:request_method]}, action=#{data[:controller_action]}")
139
+
69
140
  data
70
141
  end
71
142