sentry-rails 5.18.2 → 5.28.1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +43 -26
  3. data/Rakefile +13 -10
  4. data/app/jobs/sentry/send_event_job.rb +2 -0
  5. data/bin/console +1 -0
  6. data/bin/test +389 -0
  7. data/lib/generators/sentry_generator.rb +13 -1
  8. data/lib/sentry/rails/action_cable.rb +3 -1
  9. data/lib/sentry/rails/active_job.rb +65 -10
  10. data/lib/sentry/rails/background_worker.rb +13 -7
  11. data/lib/sentry/rails/backtrace_cleaner.rb +7 -7
  12. data/lib/sentry/rails/breadcrumb/active_support_logger.rb +2 -0
  13. data/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb +2 -0
  14. data/lib/sentry/rails/capture_exceptions.rb +4 -2
  15. data/lib/sentry/rails/configuration.rb +64 -18
  16. data/lib/sentry/rails/controller_methods.rb +2 -0
  17. data/lib/sentry/rails/controller_transaction.rb +7 -3
  18. data/lib/sentry/rails/engine.rb +2 -0
  19. data/lib/sentry/rails/error_subscriber.rb +2 -0
  20. data/lib/sentry/rails/instrument_payload_cleanup_helper.rb +2 -0
  21. data/lib/sentry/rails/log_subscriber.rb +73 -0
  22. data/lib/sentry/rails/log_subscribers/action_controller_subscriber.rb +116 -0
  23. data/lib/sentry/rails/log_subscribers/action_mailer_subscriber.rb +88 -0
  24. data/lib/sentry/rails/log_subscribers/active_job_subscriber.rb +155 -0
  25. data/lib/sentry/rails/log_subscribers/active_record_subscriber.rb +134 -0
  26. data/lib/sentry/rails/log_subscribers/parameter_filter.rb +52 -0
  27. data/lib/sentry/rails/overrides/streaming_reporter.rb +2 -0
  28. data/lib/sentry/rails/railtie.rb +16 -2
  29. data/lib/sentry/rails/rescued_exception_interceptor.rb +12 -1
  30. data/lib/sentry/rails/structured_logging.rb +32 -0
  31. data/lib/sentry/rails/tracing/abstract_subscriber.rb +2 -0
  32. data/lib/sentry/rails/tracing/action_controller_subscriber.rb +5 -3
  33. data/lib/sentry/rails/tracing/action_view_subscriber.rb +4 -2
  34. data/lib/sentry/rails/tracing/active_record_subscriber.rb +1 -0
  35. data/lib/sentry/rails/tracing/active_storage_subscriber.rb +8 -3
  36. data/lib/sentry/rails/tracing/active_support_subscriber.rb +63 -0
  37. data/lib/sentry/rails/tracing.rb +2 -0
  38. data/lib/sentry/rails/version.rb +3 -1
  39. data/lib/sentry/rails.rb +3 -0
  40. data/lib/sentry-rails.rb +2 -0
  41. data/sentry-rails.gemspec +4 -2
  42. metadata +20 -14
@@ -1,13 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class BackgroundWorker
3
- def _perform(&block)
4
- block.call
5
- ensure
6
- # some applications have partial or even no AR connection
7
- if ActiveRecord::Base.connected?
8
- # make sure the background worker returns AR connection if it accidentally acquire one during serialization
9
- ActiveRecord::Base.connection_pool.release_connection
5
+ module ActiveRecordConnectionPatch
6
+ def _perform(&block)
7
+ super(&block)
8
+ ensure
9
+ # some applications have partial or even no AR connection
10
+ if ActiveRecord::Base.connected?
11
+ # make sure the background worker returns AR connection if it accidentally acquire one during serialization
12
+ ActiveRecord::Base.connection_pool.release_connection
13
+ end
10
14
  end
11
15
  end
12
16
  end
13
17
  end
18
+
19
+ Sentry::BackgroundWorker.prepend(Sentry::BackgroundWorker::ActiveRecordConnectionPatch)
@@ -1,21 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/backtrace_cleaner"
2
4
  require "active_support/core_ext/string/access"
3
5
 
4
6
  module Sentry
5
7
  module Rails
6
8
  class BacktraceCleaner < ActiveSupport::BacktraceCleaner
7
- APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/.freeze
8
- RENDER_TEMPLATE_PATTERN = /:in `.*_\w+_{2,3}\d+_\d+'/.freeze
9
+ APP_DIRS_PATTERN = /\A(?:\.\/)?(?:app|config|lib|test|\(\w*\))/
10
+ RENDER_TEMPLATE_PATTERN = /:in (?:`|').*_\w+_{2,3}\d+_\d+'/
9
11
 
10
12
  def initialize
11
13
  super
12
- # we don't want any default silencers because they're too aggressive
14
+ # We don't want any default silencers because they're too aggressive
13
15
  remove_silencers!
16
+ # We don't want any default filters because Rails 7.2 starts shortening the paths. See #2472
17
+ remove_filters!
14
18
 
15
- @root = "#{Sentry.configuration.project_root}/"
16
- add_filter do |line|
17
- line.start_with?(@root) ? line.from(@root.size) : line
18
- end
19
19
  add_filter do |line|
20
20
  if line =~ RENDER_TEMPLATE_PATTERN
21
21
  line.sub(RENDER_TEMPLATE_PATTERN, "")
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module Breadcrumb
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sentry/rails/instrument_payload_cleanup_helper"
2
4
 
3
5
  module Sentry
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  class CaptureExceptions < Sentry::Rack::CaptureExceptions
4
6
  RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha")
5
- SPAN_ORIGIN = 'auto.http.rails'.freeze
7
+ SPAN_ORIGIN = "auto.http.rails"
6
8
 
7
9
  def initialize(_)
8
10
  super
@@ -20,7 +22,7 @@ module Sentry
20
22
  end
21
23
 
22
24
  def transaction_op
23
- "http.server".freeze
25
+ "http.server"
24
26
  end
25
27
 
26
28
  def capture_exception(exception, env)
@@ -1,25 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sentry/rails/tracing/action_controller_subscriber"
2
4
  require "sentry/rails/tracing/action_view_subscriber"
3
5
  require "sentry/rails/tracing/active_record_subscriber"
4
6
  require "sentry/rails/tracing/active_storage_subscriber"
7
+ require "sentry/rails/tracing/active_support_subscriber"
8
+
9
+ require "sentry/rails/log_subscribers/active_record_subscriber"
10
+ require "sentry/rails/log_subscribers/action_controller_subscriber"
5
11
 
6
12
  module Sentry
7
13
  class Configuration
8
14
  attr_reader :rails
9
15
 
10
- add_post_initialization_callback do
16
+ after(:initialize) do
11
17
  @rails = Sentry::Rails::Configuration.new
12
18
  @excluded_exceptions = @excluded_exceptions.concat(Sentry::Rails::IGNORE_DEFAULT)
13
19
 
14
20
  if ::Rails.logger
15
21
  if defined?(::ActiveSupport::BroadcastLogger) && ::Rails.logger.is_a?(::ActiveSupport::BroadcastLogger)
16
22
  dupped_broadcasts = ::Rails.logger.broadcasts.map(&:dup)
17
- @logger = ::ActiveSupport::BroadcastLogger.new(*dupped_broadcasts)
23
+ self.sdk_logger = ::ActiveSupport::BroadcastLogger.new(*dupped_broadcasts)
18
24
  else
19
- @logger = ::Rails.logger.dup
25
+ self.sdk_logger = ::Rails.logger.dup
20
26
  end
21
27
  else
22
- @logger.warn(Sentry::LOGGER_PROGNAME) do
28
+ sdk_logger.warn(Sentry::LOGGER_PROGNAME) do
23
29
  <<~MSG
24
30
  sentry-rails can't detect Rails.logger. it may be caused by misplacement of the SDK initialization code
25
31
  please make sure you place the Sentry.init block under the `config/initializers` folder, e.g. `config/initializers/sentry.rb`
@@ -27,24 +33,28 @@ module Sentry
27
33
  end
28
34
  end
29
35
  end
36
+
37
+ after(:configured) do
38
+ rails.structured_logging.enabled = enable_logs if rails.structured_logging.enabled.nil?
39
+ end
30
40
  end
31
41
 
32
42
  module Rails
33
43
  IGNORE_DEFAULT = [
34
- 'AbstractController::ActionNotFound',
35
- 'ActionController::BadRequest',
36
- 'ActionController::InvalidAuthenticityToken',
37
- 'ActionController::InvalidCrossOriginRequest',
38
- 'ActionController::MethodNotAllowed',
39
- 'ActionController::NotImplemented',
40
- 'ActionController::ParameterMissing',
41
- 'ActionController::RoutingError',
42
- 'ActionController::UnknownAction',
43
- 'ActionController::UnknownFormat',
44
- 'ActionDispatch::Http::MimeNegotiation::InvalidType',
45
- 'ActionController::UnknownHttpMethod',
46
- 'ActionDispatch::Http::Parameters::ParseError',
47
- 'ActiveRecord::RecordNotFound'
44
+ "AbstractController::ActionNotFound",
45
+ "ActionController::BadRequest",
46
+ "ActionController::InvalidAuthenticityToken",
47
+ "ActionController::InvalidCrossOriginRequest",
48
+ "ActionController::MethodNotAllowed",
49
+ "ActionController::NotImplemented",
50
+ "ActionController::ParameterMissing",
51
+ "ActionController::RoutingError",
52
+ "ActionController::UnknownAction",
53
+ "ActionController::UnknownFormat",
54
+ "ActionDispatch::Http::MimeNegotiation::InvalidType",
55
+ "ActionController::UnknownHttpMethod",
56
+ "ActionDispatch::Http::Parameters::ParseError",
57
+ "ActiveRecord::RecordNotFound"
48
58
  ].freeze
49
59
 
50
60
  ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT = {
@@ -153,6 +163,13 @@ module Sentry
153
163
  # @return [Hash<String, Array<Symbol>>]
154
164
  attr_accessor :active_support_logger_subscription_items
155
165
 
166
+ # Set this option to true if you want Sentry to capture each retry failure
167
+ attr_accessor :active_job_report_on_retry_error
168
+
169
+ # Configuration for structured logging feature
170
+ # @return [StructuredLoggingConfiguration]
171
+ attr_reader :structured_logging
172
+
156
173
  def initialize
157
174
  @register_error_subscriber = false
158
175
  @report_rescued_exceptions = true
@@ -162,12 +179,41 @@ module Sentry
162
179
  end
163
180
  @tracing_subscribers = Set.new([
164
181
  Sentry::Rails::Tracing::ActionViewSubscriber,
182
+ Sentry::Rails::Tracing::ActiveSupportSubscriber,
165
183
  Sentry::Rails::Tracing::ActiveRecordSubscriber,
166
184
  Sentry::Rails::Tracing::ActiveStorageSubscriber
167
185
  ])
168
186
  @enable_db_query_source = true
169
187
  @db_query_source_threshold_ms = 100
170
188
  @active_support_logger_subscription_items = Sentry::Rails::ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT.dup
189
+ @active_job_report_on_retry_error = false
190
+ @structured_logging = StructuredLoggingConfiguration.new
191
+ end
192
+ end
193
+
194
+ class StructuredLoggingConfiguration
195
+ # Enable or disable structured logging
196
+ # @return [Boolean]
197
+ attr_accessor :enabled
198
+
199
+ # Hash of components to subscriber classes for structured logging
200
+ # @return [Hash<Symbol, Class>]
201
+ attr_accessor :subscribers
202
+
203
+ DEFAULT_SUBSCRIBERS = {
204
+ active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
205
+ action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber
206
+ }.freeze
207
+
208
+ def initialize
209
+ @enabled = nil
210
+ @subscribers = DEFAULT_SUBSCRIBERS.dup
211
+ end
212
+
213
+ # Returns true if structured logging should be enabled.
214
+ # @return [Boolean]
215
+ def enabled?
216
+ enabled
171
217
  end
172
218
  end
173
219
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module ControllerMethods
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module ControllerTransaction
4
- SPAN_ORIGIN = 'auto.view.rails'.freeze
6
+ SPAN_ORIGIN = "auto.view.rails"
5
7
 
6
8
  def self.included(base)
7
9
  base.prepend_around_action(:sentry_around_action)
@@ -21,8 +23,10 @@ module Sentry
21
23
  child_span.set_http_status(response.status)
22
24
  child_span.set_data(:format, request.format)
23
25
  child_span.set_data(:method, request.method)
24
- child_span.set_data(:path, request.path)
25
- child_span.set_data(:params, request.params)
26
+
27
+ pii = Sentry.configuration.send_default_pii
28
+ child_span.set_data(:path, pii ? request.fullpath : request.filtered_path)
29
+ child_span.set_data(:params, pii ? request.params : request.filtered_parameters)
26
30
  end
27
31
 
28
32
  result
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Engine < ::Rails::Engine
3
5
  isolate_namespace Sentry
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  # This is not a user-facing class. You should use it with Rails 7.0's error reporter feature and its interfaces.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module InstrumentPayloadCleanupHelper
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ module Sentry
6
+ module Rails
7
+ # Base class for Sentry log subscribers that extends ActiveSupport::LogSubscriber
8
+ # to provide structured logging capabilities for Rails components.
9
+ #
10
+ # This class follows Rails' LogSubscriber pattern and provides common functionality
11
+ # for capturing Rails instrumentation events and logging them through Sentry's
12
+ # structured logging system.
13
+ #
14
+ # @example Creating a custom log subscriber
15
+ # class MySubscriber < Sentry::Rails::LogSubscriber
16
+ # attach_to :my_component
17
+ #
18
+ # def my_event(event)
19
+ # log_structured_event(
20
+ # message: "My event occurred",
21
+ # level: :info,
22
+ # attributes: {
23
+ # duration_ms: event.duration,
24
+ # custom_data: event.payload[:custom_data]
25
+ # }
26
+ # )
27
+ # end
28
+ # end
29
+ class LogSubscriber < ActiveSupport::LogSubscriber
30
+ ORIGIN = "auto.log.rails.log_subscriber"
31
+
32
+ class << self
33
+ if ::Rails.version.to_f < 6.0
34
+ # Rails 5.x does not provide detach_from
35
+ def detach_from(namespace, notifications = ActiveSupport::Notifications)
36
+ listeners = public_instance_methods(false)
37
+ .flat_map { |key|
38
+ notifications.notifier.listeners_for("#{key}.#{namespace}")
39
+ }
40
+ .select { |listener| listener.instance_variable_get(:@delegate).is_a?(self) }
41
+
42
+ listeners.map do |listener|
43
+ notifications.notifier.unsubscribe(listener)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ # Log a structured event using Sentry's structured logger
52
+ #
53
+ # @param message [String] The log message
54
+ # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal)
55
+ # @param attributes [Hash] Additional structured attributes to include
56
+ # @param origin [String] The origin of the log event
57
+ def log_structured_event(message:, level: :info, attributes: {}, origin: ORIGIN)
58
+ Sentry.logger.public_send(level, message, **attributes, origin: origin)
59
+ rescue => e
60
+ # Silently handle any errors in logging to avoid breaking the application
61
+ Sentry.configuration.sdk_logger.debug("Failed to log structured event: #{e.message}")
62
+ end
63
+
64
+ # Calculate duration in milliseconds from an event
65
+ #
66
+ # @param event [ActiveSupport::Notifications::Event] The event
67
+ # @return [Float] Duration in milliseconds
68
+ def duration_ms(event)
69
+ event.duration.round(2)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/rails/log_subscriber"
4
+ require "sentry/rails/log_subscribers/parameter_filter"
5
+
6
+ module Sentry
7
+ module Rails
8
+ module LogSubscribers
9
+ # LogSubscriber for ActionController events that captures HTTP request processing
10
+ # and logs them using Sentry's structured logging system.
11
+ #
12
+ # This subscriber captures process_action.action_controller events and formats them
13
+ # with relevant request information including controller, action, HTTP status,
14
+ # request parameters, and performance metrics.
15
+ #
16
+ # @example Usage
17
+ # # Enable structured logging for ActionController
18
+ # Sentry.init do |config|
19
+ # config.enable_logs = true
20
+ # config.rails.structured_logging = true
21
+ # config.rails.structured_logging.subscribers = { action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber }
22
+ # end
23
+ class ActionControllerSubscriber < Sentry::Rails::LogSubscriber
24
+ include ParameterFilter
25
+
26
+ # Handle process_action.action_controller events
27
+ #
28
+ # @param event [ActiveSupport::Notifications::Event] The controller action event
29
+ def process_action(event)
30
+ payload = event.payload
31
+
32
+ controller = payload[:controller]
33
+ action = payload[:action]
34
+
35
+ status = extract_status(payload)
36
+
37
+ attributes = {
38
+ controller: controller,
39
+ action: action,
40
+ duration_ms: duration_ms(event),
41
+ method: payload[:method],
42
+ path: payload[:path],
43
+ format: payload[:format]
44
+ }
45
+
46
+ attributes[:status] = status if status
47
+
48
+ if payload[:view_runtime]
49
+ attributes[:view_runtime_ms] = payload[:view_runtime].round(2)
50
+ end
51
+
52
+ if payload[:db_runtime]
53
+ attributes[:db_runtime_ms] = payload[:db_runtime].round(2)
54
+ end
55
+
56
+ if Sentry.configuration.send_default_pii && payload[:params]
57
+ filtered_params = filter_sensitive_params(payload[:params])
58
+ attributes[:params] = filtered_params unless filtered_params.empty?
59
+ end
60
+
61
+ level = level_for_request(payload)
62
+ message = "#{controller}##{action}"
63
+
64
+ log_structured_event(
65
+ message: message,
66
+ level: level,
67
+ attributes: attributes
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ def extract_status(payload)
74
+ if payload[:status]
75
+ payload[:status]
76
+ elsif payload[:exception]
77
+ case payload[:exception].first
78
+ when "ActionController::RoutingError"
79
+ 404
80
+ when "ActionController::BadRequest"
81
+ 400
82
+ else
83
+ 500
84
+ end
85
+ end
86
+ end
87
+
88
+ def level_for_request(payload)
89
+ status = payload[:status]
90
+
91
+ # In Rails < 6.0 status is not set when an action raised an exception
92
+ if status.nil? && payload[:exception]
93
+ case payload[:exception].first
94
+ when "ActionController::RoutingError"
95
+ :warn
96
+ when "ActionController::BadRequest"
97
+ :warn
98
+ else
99
+ :error
100
+ end
101
+ elsif status.nil?
102
+ :info
103
+ elsif status >= 200 && status < 400
104
+ :info
105
+ elsif status >= 400 && status < 500
106
+ :warn
107
+ elsif status >= 500
108
+ :error
109
+ else
110
+ :info
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/rails/log_subscriber"
4
+ require "sentry/rails/log_subscribers/parameter_filter"
5
+
6
+ module Sentry
7
+ module Rails
8
+ module LogSubscribers
9
+ # LogSubscriber for ActionMailer events that captures email delivery
10
+ # and processing events using Sentry's structured logging system.
11
+ #
12
+ # This subscriber captures deliver.action_mailer and process.action_mailer events
13
+ # and formats them with relevant email information while respecting PII settings.
14
+ #
15
+ # @example Usage
16
+ # # Enable structured logging for ActionMailer
17
+ # Sentry.init do |config|
18
+ # config.enable_logs = true
19
+ # config.rails.structured_logging = true
20
+ # config.rails.structured_logging.subscribers = { action_mailer: Sentry::Rails::LogSubscribers::ActionMailerSubscriber }
21
+ # end
22
+ class ActionMailerSubscriber < Sentry::Rails::LogSubscriber
23
+ include ParameterFilter
24
+
25
+ # Handle deliver.action_mailer events
26
+ #
27
+ # @param event [ActiveSupport::Notifications::Event] The email delivery event
28
+ def deliver(event)
29
+ payload = event.payload
30
+
31
+ mailer = payload[:mailer]
32
+
33
+ attributes = {
34
+ mailer: mailer,
35
+ duration_ms: duration_ms(event),
36
+ perform_deliveries: payload[:perform_deliveries]
37
+ }
38
+
39
+ attributes[:delivery_method] = payload[:delivery_method] if payload[:delivery_method]
40
+ attributes[:date] = payload[:date].to_s if payload[:date]
41
+
42
+ if Sentry.configuration.send_default_pii
43
+ attributes[:message_id] = payload[:message_id] if payload[:message_id]
44
+ end
45
+
46
+ message = "Email delivered via #{mailer}"
47
+
48
+ # Log the structured event
49
+ log_structured_event(
50
+ message: message,
51
+ level: :info,
52
+ attributes: attributes
53
+ )
54
+ end
55
+
56
+ # Handle process.action_mailer events
57
+ #
58
+ # @param event [ActiveSupport::Notifications::Event] The email processing event
59
+ def process(event)
60
+ payload = event.payload
61
+
62
+ mailer = payload[:mailer]
63
+ action = payload[:action]
64
+ duration = duration_ms(event)
65
+
66
+ attributes = {
67
+ mailer: mailer,
68
+ action: action,
69
+ duration_ms: duration
70
+ }
71
+
72
+ if Sentry.configuration.send_default_pii && payload[:params]
73
+ filtered_params = filter_sensitive_params(payload[:params])
74
+ attributes[:params] = filtered_params unless filtered_params.empty?
75
+ end
76
+
77
+ message = "#{mailer}##{action}"
78
+
79
+ log_structured_event(
80
+ message: message,
81
+ level: :info,
82
+ attributes: attributes
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end