sentry-rails 5.1.0 → 6.2.0

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rspec +1 -1
  4. data/Gemfile +44 -26
  5. data/README.md +1 -1
  6. data/Rakefile +13 -10
  7. data/app/jobs/sentry/send_event_job.rb +2 -1
  8. data/bin/console +1 -0
  9. data/bin/test +389 -0
  10. data/lib/generators/sentry_generator.rb +31 -0
  11. data/lib/sentry/rails/action_cable.rb +18 -7
  12. data/lib/sentry/rails/active_job.rb +118 -55
  13. data/lib/sentry/rails/background_worker.rb +13 -4
  14. data/lib/sentry/rails/backtrace_cleaner.rb +7 -7
  15. data/lib/sentry/rails/breadcrumb/active_support_logger.rb +5 -7
  16. data/lib/sentry/rails/capture_exceptions.rb +34 -15
  17. data/lib/sentry/rails/configuration.rb +171 -21
  18. data/lib/sentry/rails/controller_methods.rb +2 -0
  19. data/lib/sentry/rails/controller_transaction.rb +34 -2
  20. data/lib/sentry/rails/engine.rb +2 -0
  21. data/lib/sentry/rails/error_subscriber.rb +26 -12
  22. data/lib/sentry/rails/log_subscriber.rb +76 -0
  23. data/lib/sentry/rails/log_subscribers/action_controller_subscriber.rb +118 -0
  24. data/lib/sentry/rails/log_subscribers/action_mailer_subscriber.rb +92 -0
  25. data/lib/sentry/rails/log_subscribers/active_job_subscriber.rb +163 -0
  26. data/lib/sentry/rails/log_subscribers/active_record_subscriber.rb +164 -0
  27. data/lib/sentry/rails/log_subscribers/parameter_filter.rb +52 -0
  28. data/lib/sentry/rails/overrides/streaming_reporter.rb +2 -11
  29. data/lib/sentry/rails/railtie.rb +35 -13
  30. data/lib/sentry/rails/rescued_exception_interceptor.rb +12 -1
  31. data/lib/sentry/rails/structured_logging.rb +32 -0
  32. data/lib/sentry/rails/tracing/abstract_subscriber.rb +8 -10
  33. data/lib/sentry/rails/tracing/action_view_subscriber.rb +11 -2
  34. data/lib/sentry/rails/tracing/active_record_subscriber.rb +70 -6
  35. data/lib/sentry/rails/tracing/active_storage_subscriber.rb +17 -4
  36. data/lib/sentry/rails/tracing/active_support_subscriber.rb +63 -0
  37. data/lib/sentry/rails/tracing.rb +3 -1
  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 +16 -8
  42. metadata +26 -21
  43. data/CODE_OF_CONDUCT.md +0 -74
  44. data/lib/sentry/rails/breadcrumb/monotonic_active_support_logger.rb +0 -44
  45. data/lib/sentry/rails/instrument_payload_cleanup_helper.rb +0 -13
  46. data/lib/sentry/rails/tracing/action_controller_subscriber.rb +0 -33
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
1
5
  module Sentry
2
6
  module Rails
3
7
  module ActiveJobExtensions
@@ -5,76 +9,135 @@ module Sentry
5
9
  if !Sentry.initialized? || already_supported_by_sentry_integration?
6
10
  super
7
11
  else
8
- Sentry.with_scope do |scope|
9
- capture_and_reraise_with_sentry(scope) do
10
- super
11
- end
12
+ SentryReporter.record(self) do
13
+ super
12
14
  end
13
15
  end
14
16
  end
15
17
 
16
- def capture_and_reraise_with_sentry(scope, &block)
17
- scope.set_transaction_name(self.class.name)
18
- transaction =
19
- if is_a?(::Sentry::SendEventJob)
20
- nil
21
- else
22
- Sentry.start_transaction(name: scope.transaction_name, op: "active_job")
18
+ def already_supported_by_sentry_integration?
19
+ Sentry.configuration.rails.skippable_job_adapters.include?(self.class.queue_adapter.class.to_s)
20
+ end
21
+
22
+ class SentryReporter
23
+ OP_NAME = "queue.active_job"
24
+ SPAN_ORIGIN = "auto.queue.active_job"
25
+
26
+ EVENT_HANDLERS = {
27
+ "enqueue_retry.active_job" => :retry_handler
28
+ }
29
+
30
+ class << self
31
+ def record(job, &block)
32
+ Sentry.with_scope do |scope|
33
+ begin
34
+ scope.set_transaction_name(job.class.name, source: :task)
35
+
36
+ transaction = Sentry.start_transaction(
37
+ name: scope.transaction_name,
38
+ source: scope.transaction_source,
39
+ op: OP_NAME,
40
+ origin: SPAN_ORIGIN
41
+ )
42
+
43
+ scope.set_span(transaction) if transaction
44
+
45
+ yield.tap do
46
+ finish_sentry_transaction(transaction, 200)
47
+ end
48
+ rescue Exception => e # rubocop:disable Lint/RescueException
49
+ finish_sentry_transaction(transaction, 500)
50
+
51
+ capture_exception(job, e)
52
+
53
+ raise
54
+ end
55
+ end
23
56
  end
24
57
 
25
- scope.set_span(transaction) if transaction
58
+ def capture_exception(job, e)
59
+ Sentry::Rails.capture_exception(
60
+ e,
61
+ extra: sentry_context(job),
62
+ tags: {
63
+ job_id: job.job_id,
64
+ provider_job_id: job.provider_job_id
65
+ }
66
+ )
67
+ end
26
68
 
27
- return_value = block.call
69
+ def register_event_handlers
70
+ EVENT_HANDLERS.each do |name, handler|
71
+ subscribers << ActiveSupport::Notifications.subscribe(name) do |*args|
72
+ public_send(handler, *args)
73
+ end
74
+ end
75
+ end
28
76
 
29
- finish_sentry_transaction(transaction, 200)
77
+ def detach_event_handlers
78
+ subscribers.each do |subscriber|
79
+ ActiveSupport::Notifications.unsubscribe(subscriber)
80
+ end
81
+ subscribers.clear
82
+ end
30
83
 
31
- return_value
32
- rescue Exception => e # rubocop:disable Lint/RescueException
33
- finish_sentry_transaction(transaction, 500)
84
+ # This handler does not capture error unless `active_job_report_on_retry_error` is true
85
+ def retry_handler(*args)
86
+ handle_error_event(*args) do |job, error|
87
+ return if !Sentry.initialized? || job.already_supported_by_sentry_integration?
88
+ return unless Sentry.configuration.rails.active_job_report_on_retry_error
34
89
 
35
- Sentry::Rails.capture_exception(
36
- e,
37
- extra: sentry_context,
38
- tags: {
39
- job_id: job_id,
40
- provider_job_id: provider_job_id
41
- }
42
- )
43
- raise e
44
- end
90
+ capture_exception(job, error)
91
+ end
92
+ end
45
93
 
46
- def finish_sentry_transaction(transaction, status)
47
- return unless transaction
94
+ def handle_error_event(*args)
95
+ event = ActiveSupport::Notifications::Event.new(*args)
96
+ yield(event.payload[:job], event.payload[:error])
97
+ end
48
98
 
49
- transaction.set_http_status(status)
50
- transaction.finish
51
- end
99
+ def finish_sentry_transaction(transaction, status)
100
+ return unless transaction
52
101
 
53
- def already_supported_by_sentry_integration?
54
- Sentry.configuration.rails.skippable_job_adapters.include?(self.class.queue_adapter.class.to_s)
55
- end
102
+ transaction.set_http_status(status)
103
+ transaction.finish
104
+ end
56
105
 
57
- def sentry_context
58
- {
59
- active_job: self.class.name,
60
- arguments: sentry_serialize_arguments(arguments),
61
- scheduled_at: scheduled_at,
62
- job_id: job_id,
63
- provider_job_id: provider_job_id,
64
- locale: locale
65
- }
66
- end
106
+ def sentry_context(job)
107
+ {
108
+ active_job: job.class.name,
109
+ arguments: sentry_serialize_arguments(job.arguments),
110
+ scheduled_at: job.scheduled_at,
111
+ job_id: job.job_id,
112
+ provider_job_id: job.provider_job_id,
113
+ locale: job.locale
114
+ }
115
+ end
67
116
 
68
- def sentry_serialize_arguments(argument)
69
- case argument
70
- when Hash
71
- argument.transform_values { |v| sentry_serialize_arguments(v) }
72
- when Array, Enumerable
73
- argument.map { |v| sentry_serialize_arguments(v) }
74
- when ->(v) { v.respond_to?(:to_global_id) }
75
- argument.to_global_id.to_s rescue argument
76
- else
77
- argument
117
+ def sentry_serialize_arguments(argument)
118
+ case argument
119
+ when Range
120
+ if (argument.begin || argument.end).is_a?(ActiveSupport::TimeWithZone)
121
+ argument.to_s
122
+ else
123
+ argument.map { |v| sentry_serialize_arguments(v) }
124
+ end
125
+ when Hash
126
+ argument.transform_values { |v| sentry_serialize_arguments(v) }
127
+ when Array, Enumerable
128
+ argument.map { |v| sentry_serialize_arguments(v) }
129
+ when ->(v) { v.respond_to?(:to_global_id) }
130
+ argument.to_global_id.to_s rescue argument
131
+ else
132
+ argument
133
+ end
134
+ end
135
+
136
+ private
137
+
138
+ def subscribers
139
+ @__subscribers__ ||= Set.new
140
+ end
78
141
  end
79
142
  end
80
143
  end
@@ -1,10 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class BackgroundWorker
3
- def _perform(&block)
4
- # make sure the background worker returns AR connection if it accidentally acquire one during serialization
5
- ActiveRecord::Base.connection_pool.with_connection do
6
- block.call
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
7
14
  end
8
15
  end
9
16
  end
10
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,20 +1,16 @@
1
- require "sentry/rails/instrument_payload_cleanup_helper"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
4
  module Rails
5
5
  module Breadcrumb
6
6
  module ActiveSupportLogger
7
7
  class << self
8
- include InstrumentPayloadCleanupHelper
9
-
10
8
  def add(name, started, _finished, _unique_id, data)
11
9
  # skip Rails' internal events
12
10
  return if name.start_with?("!")
13
11
 
14
12
  if data.is_a?(Hash)
15
- # we should only mutate the copy of the data
16
- data = data.dup
17
- cleanup_data(data)
13
+ data = data.slice(*@allowed_keys[name])
18
14
  end
19
15
 
20
16
  crumb = Sentry::Breadcrumb.new(
@@ -25,7 +21,9 @@ module Sentry
25
21
  Sentry.add_breadcrumb(crumb)
26
22
  end
27
23
 
28
- def inject
24
+ def inject(allowed_keys)
25
+ @allowed_keys = allowed_keys
26
+
29
27
  @subscriber = ::ActiveSupport::Notifications.subscribe(/.*/) do |name, started, finished, unique_id, data|
30
28
  # we only record events that has a started timestamp
31
29
  if started.is_a?(Time)
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  class CaptureExceptions < Sentry::Rack::CaptureExceptions
4
- def initialize(app)
6
+ RAILS_7_1 = Gem::Version.new(::Rails.version) >= Gem::Version.new("7.1.0.alpha")
7
+ SPAN_ORIGIN = "auto.http.rails"
8
+
9
+ def initialize(_)
5
10
  super
6
11
 
7
- if defined?(::Sprockets::Rails)
8
- @assets_regex = %r(\A/{0,2}#{::Rails.application.config.assets.prefix})
12
+ if Sentry.initialized?
13
+ @assets_regexp = Sentry.configuration.rails.assets_regexp
9
14
  end
10
15
  end
11
16
 
@@ -17,29 +22,43 @@ module Sentry
17
22
  end
18
23
 
19
24
  def transaction_op
20
- "rails.request".freeze
25
+ "http.server"
21
26
  end
22
27
 
23
- def capture_exception(exception)
24
- current_scope = Sentry.get_current_scope
28
+ def capture_exception(exception, env)
29
+ # the exception will be swallowed by ShowExceptions middleware
30
+ return unless Sentry.initialized?
31
+ return if show_exceptions?(exception, env) && !Sentry.configuration.rails.report_rescued_exceptions
25
32
 
26
- if original_transaction = current_scope.rack_env["sentry.original_transaction"]
27
- current_scope.set_transaction_name(original_transaction)
33
+ Sentry::Rails.capture_exception(exception).tap do |event|
34
+ env[ERROR_EVENT_ID_KEY] = event.event_id if event
28
35
  end
29
-
30
- Sentry::Rails.capture_exception(exception)
31
36
  end
32
37
 
33
38
  def start_transaction(env, scope)
34
- sentry_trace = env["HTTP_SENTRY_TRACE"]
35
- options = { name: scope.transaction_name, op: transaction_op }
39
+ options = {
40
+ name: scope.transaction_name,
41
+ source: scope.transaction_source,
42
+ op: transaction_op,
43
+ origin: SPAN_ORIGIN
44
+ }
36
45
 
37
- if @assets_regex && scope.transaction_name.match?(@assets_regex)
46
+ if @assets_regexp && scope.transaction_name.match?(@assets_regexp)
38
47
  options.merge!(sampled: false)
39
48
  end
40
49
 
41
- transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
42
- Sentry.start_transaction(transaction: transaction, **options)
50
+ transaction = Sentry.continue_trace(env, **options)
51
+ Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
52
+ end
53
+
54
+ def show_exceptions?(exception, env)
55
+ request = ActionDispatch::Request.new(env)
56
+
57
+ if RAILS_7_1
58
+ ActionDispatch::ExceptionWrapper.new(nil, exception).show?(request)
59
+ else
60
+ request.show_exceptions?
61
+ end
43
62
  end
44
63
  end
45
64
  end
@@ -1,20 +1,30 @@
1
- require "sentry/rails/tracing/action_controller_subscriber"
1
+ # frozen_string_literal: true
2
+
2
3
  require "sentry/rails/tracing/action_view_subscriber"
3
4
  require "sentry/rails/tracing/active_record_subscriber"
4
5
  require "sentry/rails/tracing/active_storage_subscriber"
6
+ require "sentry/rails/tracing/active_support_subscriber"
7
+
8
+ require "sentry/rails/log_subscribers/active_record_subscriber"
9
+ require "sentry/rails/log_subscribers/action_controller_subscriber"
5
10
 
6
11
  module Sentry
7
12
  class Configuration
8
13
  attr_reader :rails
9
14
 
10
- add_post_initialization_callback do
15
+ after(:initialize) do
11
16
  @rails = Sentry::Rails::Configuration.new
12
17
  @excluded_exceptions = @excluded_exceptions.concat(Sentry::Rails::IGNORE_DEFAULT)
13
18
 
14
19
  if ::Rails.logger
15
- @logger = ::Rails.logger
20
+ if defined?(::ActiveSupport::BroadcastLogger) && ::Rails.logger.is_a?(::ActiveSupport::BroadcastLogger)
21
+ dupped_broadcasts = ::Rails.logger.broadcasts.map(&:dup)
22
+ self.sdk_logger = ::ActiveSupport::BroadcastLogger.new(*dupped_broadcasts)
23
+ else
24
+ self.sdk_logger = ::Rails.logger.dup
25
+ end
16
26
  else
17
- @logger.warn(Sentry::LOGGER_PROGNAME) do
27
+ sdk_logger.warn(Sentry::LOGGER_PROGNAME) do
18
28
  <<~MSG
19
29
  sentry-rails can't detect Rails.logger. it may be caused by misplacement of the SDK initialization code
20
30
  please make sure you place the Sentry.init block under the `config/initializers` folder, e.g. `config/initializers/sentry.rb`
@@ -22,30 +32,101 @@ module Sentry
22
32
  end
23
33
  end
24
34
  end
35
+
36
+ after(:configured) do
37
+ rails.structured_logging.enabled = enable_logs if rails.structured_logging.enabled.nil?
38
+ end
25
39
  end
26
40
 
27
41
  module Rails
28
42
  IGNORE_DEFAULT = [
29
- 'AbstractController::ActionNotFound',
30
- 'ActionController::BadRequest',
31
- 'ActionController::InvalidAuthenticityToken',
32
- 'ActionController::InvalidCrossOriginRequest',
33
- 'ActionController::MethodNotAllowed',
34
- 'ActionController::NotImplemented',
35
- 'ActionController::ParameterMissing',
36
- 'ActionController::RoutingError',
37
- 'ActionController::UnknownAction',
38
- 'ActionController::UnknownFormat',
39
- 'ActionDispatch::Http::MimeNegotiation::InvalidType',
40
- 'ActionController::UnknownHttpMethod',
41
- 'ActionDispatch::Http::Parameters::ParseError',
42
- 'ActiveRecord::RecordNotFound'
43
+ "AbstractController::ActionNotFound",
44
+ "ActionController::BadRequest",
45
+ "ActionController::InvalidAuthenticityToken",
46
+ "ActionController::InvalidCrossOriginRequest",
47
+ "ActionController::MethodNotAllowed",
48
+ "ActionController::NotImplemented",
49
+ "ActionController::ParameterMissing",
50
+ "ActionController::RoutingError",
51
+ "ActionController::UnknownAction",
52
+ "ActionController::UnknownFormat",
53
+ "ActionDispatch::Http::MimeNegotiation::InvalidType",
54
+ "ActionController::UnknownHttpMethod",
55
+ "ActionDispatch::Http::Parameters::ParseError",
56
+ "ActiveRecord::RecordNotFound"
43
57
  ].freeze
58
+
59
+ ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT = {
60
+ # action_controller
61
+ "write_fragment.action_controller" => %i[key],
62
+ "read_fragment.action_controller" => %i[key],
63
+ "exist_fragment?.action_controller" => %i[key],
64
+ "expire_fragment.action_controller" => %i[key],
65
+ "start_processing.action_controller" => %i[controller action params format method path],
66
+ "process_action.action_controller" => %i[controller action params format method path status view_runtime db_runtime],
67
+ "send_file.action_controller" => %i[path],
68
+ "redirect_to.action_controller" => %i[status location],
69
+ "halted_callback.action_controller" => %i[filter],
70
+ # action_dispatch
71
+ "process_middleware.action_dispatch" => %i[middleware],
72
+ # action_view
73
+ "render_template.action_view" => %i[identifier layout],
74
+ "render_partial.action_view" => %i[identifier],
75
+ "render_collection.action_view" => %i[identifier count cache_hits],
76
+ "render_layout.action_view" => %i[identifier],
77
+ # active_record
78
+ "sql.active_record" => %i[sql name statement_name cached],
79
+ "instantiation.active_record" => %i[record_count class_name],
80
+ # action_mailer
81
+ # not including to, from, or subject..etc. because of PII concern
82
+ "deliver.action_mailer" => %i[mailer date perform_deliveries],
83
+ "process.action_mailer" => %i[mailer action params],
84
+ # active_support
85
+ "cache_read.active_support" => %i[key store hit],
86
+ "cache_generate.active_support" => %i[key store],
87
+ "cache_fetch_hit.active_support" => %i[key store],
88
+ "cache_write.active_support" => %i[key store],
89
+ "cache_delete.active_support" => %i[key store],
90
+ "cache_exist?.active_support" => %i[key store],
91
+ # active_job
92
+ "enqueue_at.active_job" => %i[],
93
+ "enqueue.active_job" => %i[],
94
+ "enqueue_retry.active_job" => %i[],
95
+ "perform_start.active_job" => %i[],
96
+ "perform.active_job" => %i[],
97
+ "retry_stopped.active_job" => %i[],
98
+ "discard.active_job" => %i[],
99
+ # action_cable
100
+ "perform_action.action_cable" => %i[channel_class action],
101
+ "transmit.action_cable" => %i[channel_class],
102
+ "transmit_subscription_confirmation.action_cable" => %i[channel_class],
103
+ "transmit_subscription_rejection.action_cable" => %i[channel_class],
104
+ "broadcast.action_cable" => %i[broadcasting],
105
+ # active_storage
106
+ "service_upload.active_storage" => %i[service key checksum],
107
+ "service_streaming_download.active_storage" => %i[service key],
108
+ "service_download_chunk.active_storage" => %i[service key],
109
+ "service_download.active_storage" => %i[service key],
110
+ "service_delete.active_storage" => %i[service key],
111
+ "service_delete_prefixed.active_storage" => %i[service prefix],
112
+ "service_exist.active_storage" => %i[service key exist],
113
+ "service_url.active_storage" => %i[service key url],
114
+ "service_update_metadata.active_storage" => %i[service key],
115
+ "preview.active_storage" => %i[key],
116
+ "analyze.active_storage" => %i[analyzer]
117
+ }.freeze
118
+
44
119
  class Configuration
120
+ # Rails 7.0 introduced a new error reporter feature, which the SDK once opted-in by default.
121
+ # But after receiving multiple issue reports, the integration seemed to cause serious troubles to some users.
122
+ # So the integration is now controlled by this configuration, which is disabled (false) by default.
123
+ # More information can be found from: https://github.com/rails/rails/pull/43625#issuecomment-1072514175
124
+ attr_accessor :register_error_subscriber
125
+
45
126
  # Rails catches exceptions in the ActionDispatch::ShowExceptions or
46
127
  # ActionDispatch::DebugExceptions middlewares, depending on the environment.
47
- # When `rails_report_rescued_exceptions` is true (it is by default), Sentry
48
- # will report exceptions even when they are rescued by these middlewares.
128
+ # When `report_rescued_exceptions` is true (it is by default), Sentry will
129
+ # report exceptions even when they are rescued by these middlewares.
49
130
  attr_accessor :report_rescued_exceptions
50
131
 
51
132
  # Some adapters, like sidekiq, already have their own sentry integration.
@@ -54,15 +135,84 @@ module Sentry
54
135
 
55
136
  attr_accessor :tracing_subscribers
56
137
 
138
+ # When the ActiveRecordSubscriber is enabled, capture the source location of the query in the span data.
139
+ # This is enabled by default, but can be disabled by setting this to false.
140
+ attr_accessor :enable_db_query_source
141
+
142
+ # The threshold in milliseconds for the ActiveRecordSubscriber to capture the source location of the query
143
+ # in the span data. Default is 100ms.
144
+ attr_accessor :db_query_source_threshold_ms
145
+
146
+ # sentry-rails by default skips asset request' transactions by checking if the path matches
147
+ #
148
+ # ```rb
149
+ # %r(\A/{0,2}#{::Rails.application.config.assets.prefix})
150
+ # ```
151
+ #
152
+ # If you want to use a different pattern, you can configure the `assets_regexp` option like:
153
+ #
154
+ # ```rb
155
+ # Sentry.init do |config|
156
+ # config.rails.assets_regexp = /my_regexp/
157
+ # end
158
+ # ```
159
+ attr_accessor :assets_regexp
160
+
161
+ # Hash of subscription items that will be shown in breadcrumbs active support logger.
162
+ # @return [Hash<String, Array<Symbol>>]
163
+ attr_accessor :active_support_logger_subscription_items
164
+
165
+ # Set this option to true if you want Sentry to capture each retry failure
166
+ attr_accessor :active_job_report_on_retry_error
167
+
168
+ # Configuration for structured logging feature
169
+ # @return [StructuredLoggingConfiguration]
170
+ attr_reader :structured_logging
171
+
57
172
  def initialize
173
+ @register_error_subscriber = false
58
174
  @report_rescued_exceptions = true
59
175
  @skippable_job_adapters = []
176
+ @assets_regexp = if defined?(::Sprockets::Rails)
177
+ %r(\A/{0,2}#{::Rails.application.config.assets.prefix})
178
+ end
60
179
  @tracing_subscribers = Set.new([
61
- Sentry::Rails::Tracing::ActionControllerSubscriber,
62
180
  Sentry::Rails::Tracing::ActionViewSubscriber,
181
+ Sentry::Rails::Tracing::ActiveSupportSubscriber,
63
182
  Sentry::Rails::Tracing::ActiveRecordSubscriber,
64
183
  Sentry::Rails::Tracing::ActiveStorageSubscriber
65
184
  ])
185
+ @enable_db_query_source = true
186
+ @db_query_source_threshold_ms = 100
187
+ @active_support_logger_subscription_items = Sentry::Rails::ACTIVE_SUPPORT_LOGGER_SUBSCRIPTION_ITEMS_DEFAULT.dup
188
+ @active_job_report_on_retry_error = false
189
+ @structured_logging = StructuredLoggingConfiguration.new
190
+ end
191
+ end
192
+
193
+ class StructuredLoggingConfiguration
194
+ # Enable or disable structured logging
195
+ # @return [Boolean]
196
+ attr_accessor :enabled
197
+
198
+ # Hash of components to subscriber classes for structured logging
199
+ # @return [Hash<Symbol, Class>]
200
+ attr_accessor :subscribers
201
+
202
+ DEFAULT_SUBSCRIBERS = {
203
+ active_record: Sentry::Rails::LogSubscribers::ActiveRecordSubscriber,
204
+ action_controller: Sentry::Rails::LogSubscribers::ActionControllerSubscriber
205
+ }.freeze
206
+
207
+ def initialize
208
+ @enabled = nil
209
+ @subscribers = DEFAULT_SUBSCRIBERS.dup
210
+ end
211
+
212
+ # Returns true if structured logging should be enabled.
213
+ # @return [Boolean]
214
+ def enabled?
215
+ enabled
66
216
  end
67
217
  end
68
218
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module ControllerMethods
@@ -1,9 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Rails
3
5
  module ControllerTransaction
6
+ SPAN_ORIGIN = "auto.view.rails"
7
+
4
8
  def self.included(base)
5
- base.prepend_before_action do |controller|
6
- Sentry.get_current_scope.set_transaction_name("#{controller.class}##{controller.action_name}")
9
+ base.prepend_around_action(:sentry_around_action)
10
+ end
11
+
12
+ private
13
+
14
+ def sentry_around_action
15
+ if Sentry.initialized?
16
+ transaction_name = "#{self.class}##{action_name}"
17
+ Sentry.get_current_scope.set_transaction_name(transaction_name, source: :view)
18
+ Sentry.with_child_span(op: "view.process_action.action_controller", description: transaction_name, origin: SPAN_ORIGIN) do |child_span|
19
+ if child_span
20
+ begin
21
+ result = yield
22
+ ensure
23
+ child_span.set_http_status(response.status)
24
+ child_span.set_data(:format, request.format)
25
+ child_span.set_data(:method, request.method)
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)
30
+ end
31
+
32
+ result
33
+ else
34
+ yield
35
+ end
36
+ end
37
+ else
38
+ yield
7
39
  end
8
40
  end
9
41
  end
@@ -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