activerabbit-ai 0.4.1 → 0.4.4
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 +36 -0
- data/README.md +59 -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 +16 -3
- data/lib/active_rabbit/client/dedupe.rb +42 -0
- data/lib/active_rabbit/client/error_reporter.rb +89 -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 +488 -44
- 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 +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86853d97a830e8d411ba9052ff231b29a2cddb526799e6b5c89e848ce026c7b2
|
|
4
|
+
data.tar.gz: 1faeb9a59503089d03b0ecda80fab72cd8028b3bfd49f4da5c5d25ff9cfb3139
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 760d010f75113d98d6e2f83fd3768d37d7cf67e307c24418717caa8c07ea489337ed7923ef876ed7bb95e4556e6780466efaec374d2ffc62d00b7b16a37f6fda
|
|
7
|
+
data.tar.gz: 441b28d1cc42c02a13c3efe4d387674932fceafca7c8571c6b24792ac69134338050ad030ebe65253ee48387e108b8b7d85014866bd42fed1e56f1c9fa356647
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.4] - 2025-10-22
|
|
6
|
+
|
|
7
|
+
### Improved
|
|
8
|
+
- **Time-based error deduplication**: Changed from "once per server lifecycle" to time-window based
|
|
9
|
+
- Added `dedupe_window` configuration option (default: 300 seconds / 5 minutes)
|
|
10
|
+
- Set `dedupe_window` to `0` to disable deduplication (useful for development/testing)
|
|
11
|
+
- Automatic memory cleanup of old dedupe entries (keeps last hour only)
|
|
12
|
+
- Better logging when errors are deduplicated
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Error deduplication no longer prevents same error from being reported after time window expires
|
|
16
|
+
- Memory leak prevention by cleaning old deduplication entries
|
|
17
|
+
|
|
18
|
+
## [0.4.3] - 2025-10-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Critical bug fix**: Removed reference to non-existent `ActiveRabbit::Client::Dedupe` class in error reporter
|
|
22
|
+
- Error tracking now works properly via Rails error reporter integration
|
|
23
|
+
- Errors are successfully captured and sent to ActiveRabbit API
|
|
24
|
+
|
|
25
|
+
## [0.4.2] - 2025-01-04
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- **Major Rails 6.1 compatibility fix**: Added Rails Engine as primary integration method
|
|
29
|
+
- Engine loads after Rails initialization (safer than Railtie)
|
|
30
|
+
- Added comprehensive error handling for Rails loading edge cases
|
|
31
|
+
- Fallback mechanism: Engine -> Railtie -> Graceful degradation
|
|
32
|
+
- Fixed Logger initialization issues in Docker environments
|
|
33
|
+
- Added better error messages for debugging Rails integration issues
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- Rails Engine integration (`ActiveRabbit::Client::Engine`)
|
|
37
|
+
- Safer middleware insertion with error handling
|
|
38
|
+
- Enhanced shutdown hooks and signal handling
|
|
39
|
+
- Better request context management
|
|
40
|
+
|
|
5
41
|
## [0.4.1] - 2025-01-04
|
|
6
42
|
|
|
7
43
|
### Fixed
|
data/README.md
CHANGED
|
@@ -236,6 +236,65 @@ ActiveRabbit::Client.configure do |config|
|
|
|
236
236
|
end
|
|
237
237
|
```
|
|
238
238
|
|
|
239
|
+
### Recommended Exceptions to Capture
|
|
240
|
+
|
|
241
|
+
Below is a practical list of exceptions APMs should capture by default in a Rails app. Some are optional/noisy and typically excluded unless needed.
|
|
242
|
+
|
|
243
|
+
- Core (Ruby/Stdlib)
|
|
244
|
+
- `StandardError`, `RuntimeError`, `NoMethodError`, `NameError`, `ArgumentError`, `TypeError`, `IndexError`, `KeyError`
|
|
245
|
+
- `Timeout::Error`, `JSON::ParserError`, `OpenSSL::SSL::SSLError`, `SocketError`, `Errno::ECONNREFUSED`/`ETIMEDOUT`/`EHOSTUNREACH`
|
|
246
|
+
|
|
247
|
+
- ActionPack / Controllers
|
|
248
|
+
- `ActionController::ParameterMissing`
|
|
249
|
+
- `ActionController::BadRequest`
|
|
250
|
+
- `ActionController::InvalidAuthenticityToken` (optional; can be noisy)
|
|
251
|
+
- `ActionController::UnknownFormat`
|
|
252
|
+
- `ActionController::NotImplemented`
|
|
253
|
+
|
|
254
|
+
- Routing (optional/noisy)
|
|
255
|
+
- `ActionController::RoutingError` (commonly excluded; enable only if needed)
|
|
256
|
+
|
|
257
|
+
- Views / Templates
|
|
258
|
+
- `ActionView::Template::Error`
|
|
259
|
+
- `ActionView::MissingTemplate`
|
|
260
|
+
- `Encoding::UndefinedConversionError` (template rendering)
|
|
261
|
+
|
|
262
|
+
- ActiveRecord / Database
|
|
263
|
+
- `ActiveRecord::RecordInvalid`
|
|
264
|
+
- `ActiveRecord::RecordNotFound` (optional; may be expected business logic)
|
|
265
|
+
- `ActiveRecord::StatementInvalid` (includes `PG::Error` subclasses)
|
|
266
|
+
- `ActiveRecord::Deadlocked`, `ActiveRecord::LockWaitTimeout`
|
|
267
|
+
- `ActiveRecord::RecordNotUnique`
|
|
268
|
+
- `ActiveRecord::ConnectionTimeoutError`
|
|
269
|
+
- `ActiveRecord::SerializationFailure`
|
|
270
|
+
|
|
271
|
+
- Background Jobs (ActiveJob/Sidekiq)
|
|
272
|
+
- `ActiveJob::DeserializationError`
|
|
273
|
+
- Any unhandled exception raised in job `perform`
|
|
274
|
+
|
|
275
|
+
- Networking/HTTP Clients
|
|
276
|
+
- `Net::OpenTimeout`, `Net::ReadTimeout`
|
|
277
|
+
- `Faraday::TimeoutError`, `Faraday::ConnectionFailed`
|
|
278
|
+
- `HTTP::Error` (http.rb), `RestClient::Exception`
|
|
279
|
+
|
|
280
|
+
- Caching/Redis
|
|
281
|
+
- `Redis::BaseError`, `Redis::TimeoutError`, `Redis::CannotConnectError`
|
|
282
|
+
|
|
283
|
+
- ActiveStorage
|
|
284
|
+
- `ActiveStorage::IntegrityError`
|
|
285
|
+
- `ActiveStorage::FileNotFoundError`
|
|
286
|
+
|
|
287
|
+
- ActionCable
|
|
288
|
+
- `ActionCable::Connection::Authorization::UnauthorizedError` (if applicable)
|
|
289
|
+
|
|
290
|
+
- Security/Crypto
|
|
291
|
+
- `ActiveSupport::MessageEncryptor::InvalidMessage`
|
|
292
|
+
- `ActiveSupport::MessageVerifier::InvalidSignature`
|
|
293
|
+
|
|
294
|
+
Notes:
|
|
295
|
+
- Optional/noisy: `RoutingError`, `RecordNotFound`, `InvalidAuthenticityToken`. Consider monitoring via metrics or targeted capture.
|
|
296
|
+
- If exceptions are rescued by your app, enable reporting of rescued exceptions (`before_send_exception`/custom middleware) so they are still tracked when appropriate.
|
|
297
|
+
|
|
239
298
|
### Callbacks
|
|
240
299
|
|
|
241
300
|
```ruby
|
|
@@ -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,13 @@ 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
|
+
attr_accessor :dedupe_window # Time window in seconds for error deduplication (0 = disabled)
|
|
17
18
|
|
|
18
19
|
def initialize
|
|
19
|
-
@api_url = ENV.fetch("active_rabbit_API_URL", "https://api.activerabbit.
|
|
20
|
+
@api_url = ENV.fetch("active_rabbit_API_URL", "https://api.activerabbit.ai")
|
|
20
21
|
@api_key = ENV["active_rabbit_API_KEY"]
|
|
21
22
|
@project_id = ENV["active_rabbit_PROJECT_ID"]
|
|
22
23
|
@environment = ENV.fetch("active_rabbit_ENVIRONMENT", detect_environment)
|
|
@@ -45,9 +46,10 @@ module ActiveRabbit
|
|
|
45
46
|
]
|
|
46
47
|
|
|
47
48
|
# Filtering
|
|
49
|
+
# default ignores (404 controlled by ignore_404)
|
|
50
|
+
@ignore_404 = true
|
|
48
51
|
@ignored_exceptions = %w[
|
|
49
52
|
ActiveRecord::RecordNotFound
|
|
50
|
-
ActionController::RoutingError
|
|
51
53
|
ActionController::InvalidAuthenticityToken
|
|
52
54
|
CGI::Session::CookieStore::TamperedWithCookie
|
|
53
55
|
]
|
|
@@ -59,6 +61,9 @@ module ActiveRabbit
|
|
|
59
61
|
/Twitterbot/i
|
|
60
62
|
]
|
|
61
63
|
|
|
64
|
+
# Deduplication (0 = disabled, time in seconds for same error to be considered duplicate)
|
|
65
|
+
@dedupe_window = 300 # 5 minutes by default
|
|
66
|
+
|
|
62
67
|
# Metadata
|
|
63
68
|
@release = detect_release
|
|
64
69
|
@server_name = detect_server_name
|
|
@@ -83,6 +88,14 @@ module ActiveRabbit
|
|
|
83
88
|
|
|
84
89
|
def should_ignore_exception?(exception)
|
|
85
90
|
return false unless exception
|
|
91
|
+
# Special-case 404 via flag
|
|
92
|
+
if @ignore_404
|
|
93
|
+
begin
|
|
94
|
+
return true if exception.is_a?(ActionController::RoutingError)
|
|
95
|
+
rescue NameError
|
|
96
|
+
# Ignore if AC not loaded
|
|
97
|
+
end
|
|
98
|
+
end
|
|
86
99
|
|
|
87
100
|
ignored_exceptions.any? do |ignored|
|
|
88
101
|
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,89 @@
|
|
|
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
|
+
# Time-based deduplication: track errors with timestamps
|
|
14
|
+
$reported_errors ||= {}
|
|
15
|
+
|
|
16
|
+
# Generate a unique key for this error
|
|
17
|
+
error_key = "#{exception.class.name}:#{exception.message}:#{exception.backtrace&.first}"
|
|
18
|
+
|
|
19
|
+
# Get dedupe window from config (default 5 minutes, 0 = disabled)
|
|
20
|
+
dedupe_window = defined?(ActiveRabbit::Client.configuration.dedupe_window) ?
|
|
21
|
+
ActiveRabbit::Client.configuration.dedupe_window : 300
|
|
22
|
+
|
|
23
|
+
current_time = Time.now.to_i
|
|
24
|
+
last_seen = $reported_errors[error_key]
|
|
25
|
+
|
|
26
|
+
# Report if: never seen before, OR dedupe disabled (0), OR outside dedupe window
|
|
27
|
+
should_report = last_seen.nil? || dedupe_window == 0 || (current_time - last_seen) > dedupe_window
|
|
28
|
+
|
|
29
|
+
if should_report
|
|
30
|
+
$reported_errors[error_key] = current_time
|
|
31
|
+
|
|
32
|
+
# Clean old entries to prevent memory leak (keep last hour)
|
|
33
|
+
$reported_errors.delete_if { |_, timestamp| current_time - timestamp > 3600 }
|
|
34
|
+
|
|
35
|
+
enriched = build_enriched_context(exception, handled: handled, severity: severity, context: context)
|
|
36
|
+
ActiveRabbit::Client.track_exception(exception, handled: handled, context: enriched)
|
|
37
|
+
else
|
|
38
|
+
Rails.logger.debug "[ActiveRabbit] Error deduplicated (last seen #{current_time - last_seen}s ago)" if defined?(Rails.logger)
|
|
39
|
+
end
|
|
40
|
+
rescue => e
|
|
41
|
+
Rails.logger.error "[ActiveRabbit] Error in ErrorReporter::Subscriber#report: #{e.class} - #{e.message}" if defined?(Rails.logger)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_enriched_context(exception, handled:, severity:, context: {})
|
|
48
|
+
ctx = { handled: handled, severity: severity, source: 'rails_error_reporter' }
|
|
49
|
+
ctx[:framework_context] = context || {}
|
|
50
|
+
|
|
51
|
+
env = context && (context[:env] || context['env'])
|
|
52
|
+
if env
|
|
53
|
+
req_info = ActiveRabbit::Reporting.rack_request_info(env)
|
|
54
|
+
ctx[:request] = req_info[:request]
|
|
55
|
+
ctx[:routing] = req_info[:routing]
|
|
56
|
+
# Top-level convenience for UI
|
|
57
|
+
ctx[:request_path] = ctx[:request][:path]
|
|
58
|
+
ctx[:request_method] = ctx[:request][:method]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if defined?(ActionController::RoutingError) && exception.is_a?(ActionController::RoutingError)
|
|
62
|
+
ctx[:controller_action] = 'Routing#not_found'
|
|
63
|
+
ctx[:error_type] = 'route_not_found'
|
|
64
|
+
ctx[:error_status] = 404
|
|
65
|
+
ctx[:error_component] = 'ActionDispatch'
|
|
66
|
+
ctx[:error_source] = 'Router'
|
|
67
|
+
ctx[:tags] = (ctx[:tags] || {}).merge(error_type: 'routing_error', severity: 'warning')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
ctx
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.attach!
|
|
75
|
+
# Rails 7.0+: Rails.error; earlier versions no-op
|
|
76
|
+
if defined?(Rails) && Rails.respond_to?(:error)
|
|
77
|
+
Rails.logger.info "[ActiveRabbit] Attaching to Rails error reporter" if defined?(Rails.logger)
|
|
78
|
+
|
|
79
|
+
subscriber = Subscriber.new
|
|
80
|
+
Rails.error.subscribe(subscriber)
|
|
81
|
+
|
|
82
|
+
Rails.logger.info "[ActiveRabbit] Rails error reporter attached successfully" if defined?(Rails.logger)
|
|
83
|
+
else
|
|
84
|
+
Rails.logger.info "[ActiveRabbit] Rails error reporter not available (Rails < 7.0)" if defined?(Rails.logger)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
# Required fields
|
|
83
|
+
exception_class: exception.class.name,
|
|
47
84
|
message: exception.message,
|
|
48
|
-
backtrace:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|