sentry-ruby 5.14.0 → 5.16.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95fd53222ce5360d32645f0bc7577792bdef0a02b9e4b8de27555eabd2ef1844
4
- data.tar.gz: e5eb696315a22747051e5bbdeac314c5ae103cabd59d3fb266b16b0b22345611
3
+ metadata.gz: 565289974a80625ef7ecac6926cfb7292569dfafd84f12584a8214619bdd5ed5
4
+ data.tar.gz: 005b3e5c27b7188058c412d36981779eb759f7be1032429eea17a2e4a7ffcb9f
5
5
  SHA512:
6
- metadata.gz: f18df0d05208c0a03352501b4b746f6ccc74799b234198b67b35e709539fde52aa36c56a6297510e8e9e1dc8a62658ac5d81e0023c5935de455effc16ee824fe
7
- data.tar.gz: 47f5db68454ab971d92df8c7fbd95846830edfe09b6b2ea40fef1f5ab39a9db922a5564abe45b29fb55df729bdf53bb1632c566c93b8b7f0d1c96e00c441d0dc
6
+ metadata.gz: d0e68db7961263d6aa070feaa131fae4c2039884e565cd248795298a99f08d66e7c921df1a6f6175e68ac0ed554185b4b94769f8b0fcba12d76e6aa042fc3359
7
+ data.tar.gz: 5e10f1406d7a7809aed02cef80e1d4355e61af54ed42ffd9a1b3a54a730229abfef4f0a690980c646757b93fffa6219d6500c5700b08a6eef61b6eb682d63620
data/Gemfile CHANGED
@@ -12,28 +12,9 @@ gem "redis", "~> #{redis_rb_version}"
12
12
 
13
13
  gem "puma"
14
14
 
15
- gem "rake", "~> 12.0"
16
- gem "rspec", "~> 3.0"
17
- gem "rspec-retry"
18
15
  gem "timecop"
19
- gem "simplecov"
20
- gem "simplecov-cobertura", "~> 1.4"
21
- gem "rexml"
22
16
  gem "stackprof" unless RUBY_PLATFORM == "java"
23
17
 
24
- ruby_version = Gem::Version.new(RUBY_VERSION)
25
-
26
- if ruby_version >= Gem::Version.new("2.6.0")
27
- gem "debug", github: "ruby/debug", platform: :ruby
28
- gem "irb"
29
-
30
- if ruby_version >= Gem::Version.new("3.0.0")
31
- gem "ruby-lsp-rspec"
32
- end
33
- end
34
-
35
- gem "pry"
36
-
37
18
  gem "benchmark-ips"
38
19
  gem "benchmark_driver"
39
20
  gem "benchmark-ipsa"
@@ -41,3 +22,5 @@ gem "benchmark-memory"
41
22
 
42
23
  gem "yard", github: "lsegal/yard"
43
24
  gem "webrick"
25
+
26
+ eval_gemfile File.expand_path("../Gemfile", __dir__)
@@ -13,10 +13,12 @@ module Sentry
13
13
  attr_reader :logger
14
14
  attr_accessor :shutdown_timeout
15
15
 
16
+ DEFAULT_MAX_QUEUE = 30
17
+
16
18
  def initialize(configuration)
17
- @max_queue = 30
18
19
  @shutdown_timeout = 1
19
20
  @number_of_threads = configuration.background_worker_threads
21
+ @max_queue = configuration.background_worker_max_queue
20
22
  @logger = configuration.logger
21
23
  @debug = configuration.debug
22
24
  @shutdown_callback = nil
@@ -63,6 +65,11 @@ module Sentry
63
65
  @shutdown_callback&.call
64
66
  end
65
67
 
68
+ def full?
69
+ @executor.is_a?(Concurrent::ThreadPoolExecutor) &&
70
+ @executor.remaining_capacity == 0
71
+ end
72
+
66
73
  private
67
74
 
68
75
  def _perform(&block)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class BackpressureMonitor
5
+ include LoggingHelper
6
+
7
+ DEFAULT_INTERVAL = 10
8
+ MAX_DOWNSAMPLE_FACTOR = 10
9
+
10
+ def initialize(configuration, client, interval: DEFAULT_INTERVAL)
11
+ @interval = interval
12
+ @client = client
13
+ @logger = configuration.logger
14
+
15
+ @thread = nil
16
+ @exited = false
17
+
18
+ @healthy = true
19
+ @downsample_factor = 0
20
+ end
21
+
22
+ def healthy?
23
+ ensure_thread
24
+ @healthy
25
+ end
26
+
27
+ def downsample_factor
28
+ ensure_thread
29
+ @downsample_factor
30
+ end
31
+
32
+ def run
33
+ check_health
34
+ set_downsample_factor
35
+ end
36
+
37
+ def check_health
38
+ @healthy = !(@client.transport.any_rate_limited? || Sentry.background_worker&.full?)
39
+ end
40
+
41
+ def set_downsample_factor
42
+ if @healthy
43
+ log_debug("[BackpressureMonitor] health check positive, reverting to normal sampling") if @downsample_factor.positive?
44
+ @downsample_factor = 0
45
+ else
46
+ @downsample_factor += 1 if @downsample_factor < MAX_DOWNSAMPLE_FACTOR
47
+ log_debug("[BackpressureMonitor] health check negative, downsampling with a factor of #{@downsample_factor}")
48
+ end
49
+ end
50
+
51
+ def kill
52
+ log_debug("[BackpressureMonitor] killing monitor")
53
+
54
+ @exited = true
55
+ @thread&.kill
56
+ end
57
+
58
+ private
59
+
60
+ def ensure_thread
61
+ return if @exited
62
+ return if @thread&.alive?
63
+
64
+ @thread = Thread.new do
65
+ loop do
66
+ sleep(@interval)
67
+ run
68
+ end
69
+ end
70
+ rescue ThreadError
71
+ log_debug("[BackpressureMonitor] Thread creation failed")
72
+ @exited = true
73
+ end
74
+ end
75
+ end
data/lib/sentry/client.rb CHANGED
@@ -10,6 +10,10 @@ module Sentry
10
10
  # @return [Transport]
11
11
  attr_reader :transport
12
12
 
13
+ # The Transport object that'll send events for the client.
14
+ # @return [SpotlightTransport, nil]
15
+ attr_reader :spotlight_transport
16
+
13
17
  # @!macro configuration
14
18
  attr_reader :configuration
15
19
 
@@ -32,6 +36,8 @@ module Sentry
32
36
  DummyTransport.new(configuration)
33
37
  end
34
38
  end
39
+
40
+ @spotlight_transport = SpotlightTransport.new(configuration) if configuration.spotlight
35
41
  end
36
42
 
37
43
  # Applies the given scope's data to the event and sends it to Sentry.
@@ -42,7 +48,7 @@ module Sentry
42
48
  def capture_event(event, scope, hint = {})
43
49
  return unless configuration.sending_allowed?
44
50
 
45
- unless event.is_a?(TransactionEvent) || configuration.sample_allowed?
51
+ if event.is_a?(ErrorEvent) && !configuration.sample_allowed?
46
52
  transport.record_lost_event(:sample_rate, 'event')
47
53
  return
48
54
  end
@@ -51,7 +57,7 @@ module Sentry
51
57
  event = scope.apply_to_event(event, hint)
52
58
 
53
59
  if event.nil?
54
- log_info("Discarded event because one of the event processors returned nil")
60
+ log_debug("Discarded event because one of the event processors returned nil")
55
61
  transport.record_lost_event(:event_processor, event_type)
56
62
  return
57
63
  end
@@ -150,7 +156,7 @@ module Sentry
150
156
  event = configuration.before_send.call(event, hint)
151
157
 
152
158
  if event.nil?
153
- log_info("Discarded event because before_send returned nil")
159
+ log_debug("Discarded event because before_send returned nil")
154
160
  transport.record_lost_event(:before_send, 'event')
155
161
  return
156
162
  end
@@ -160,21 +166,18 @@ module Sentry
160
166
  event = configuration.before_send_transaction.call(event, hint)
161
167
 
162
168
  if event.nil?
163
- log_info("Discarded event because before_send_transaction returned nil")
169
+ log_debug("Discarded event because before_send_transaction returned nil")
164
170
  transport.record_lost_event(:before_send, 'transaction')
165
171
  return
166
172
  end
167
173
  end
168
174
 
169
175
  transport.send_event(event)
176
+ spotlight_transport&.send_event(event)
170
177
 
171
178
  event
172
179
  rescue => e
173
- loggable_event_type = event_type.capitalize
174
- log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
175
-
176
- event_info = Event.get_log_message(event.to_hash)
177
- log_info("Unreported #{loggable_event_type}: #{event_info}")
180
+ log_error("Event sending failed", e, debug: configuration.debug)
178
181
  transport.record_lost_event(:network_error, event_type)
179
182
  raise
180
183
  end
@@ -7,6 +7,7 @@ require 'sentry/utils/custom_inspection'
7
7
  require "sentry/dsn"
8
8
  require "sentry/release_detector"
9
9
  require "sentry/transport/configuration"
10
+ require "sentry/cron/configuration"
10
11
  require "sentry/linecache"
11
12
  require "sentry/interfaces/stacktrace_builder"
12
13
 
@@ -40,6 +41,13 @@ module Sentry
40
41
  # @return [Integer]
41
42
  attr_accessor :background_worker_threads
42
43
 
44
+ # The maximum queue size for the background worker.
45
+ # Jobs will be rejected above this limit.
46
+ #
47
+ # Default is {BackgroundWorker::DEFAULT_MAX_QUEUE}.
48
+ # @return [Integer]
49
+ attr_accessor :background_worker_max_queue
50
+
43
51
  # a proc/lambda that takes an array of stack traces
44
52
  # it'll be used to silence (reduce) backtrace of the exception
45
53
  #
@@ -142,6 +150,14 @@ module Sentry
142
150
  # @return [Boolean]
143
151
  attr_accessor :include_local_variables
144
152
 
153
+ # Whether to capture events and traces into Spotlight. Default is false.
154
+ # If you set this to true, Sentry will send events and traces to the local
155
+ # Sidecar proxy at http://localhost:8969/stream.
156
+ # If you want to use a different Sidecar proxy address, set this to String
157
+ # with the proxy URL.
158
+ # @return [Boolean, String]
159
+ attr_accessor :spotlight
160
+
145
161
  # @deprecated Use {#include_local_variables} instead.
146
162
  alias_method :capture_exception_frame_locals, :include_local_variables
147
163
 
@@ -211,10 +227,14 @@ module Sentry
211
227
  # @return [String]
212
228
  attr_accessor :server_name
213
229
 
214
- # Return a Transport::Configuration object for transport-related configurations.
215
- # @return [Transport]
230
+ # Transport related configuration.
231
+ # @return [Transport::Configuration]
216
232
  attr_reader :transport
217
233
 
234
+ # Cron related configuration.
235
+ # @return [Cron::Configuration]
236
+ attr_reader :cron
237
+
218
238
  # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
219
239
  # @return [Float, nil]
220
240
  attr_reader :traces_sample_rate
@@ -243,6 +263,12 @@ module Sentry
243
263
  # @return [Boolean]
244
264
  attr_accessor :auto_session_tracking
245
265
 
266
+ # Whether to downsample transactions automatically because of backpressure.
267
+ # Starts a new monitor thread to check health of the SDK every 10 seconds.
268
+ # Default is false
269
+ # @return [Boolean]
270
+ attr_accessor :enable_backpressure_handling
271
+
246
272
  # Allowlist of outgoing request targets to which sentry-trace and baggage headers are attached.
247
273
  # Default is all (/.*/)
248
274
  # @return [Array<String, Regexp>]
@@ -321,6 +347,7 @@ module Sentry
321
347
  self.app_dirs_pattern = nil
322
348
  self.debug = false
323
349
  self.background_worker_threads = Concurrent.processor_count
350
+ self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE
324
351
  self.backtrace_cleanup_callback = nil
325
352
  self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
326
353
  self.breadcrumbs_logger = []
@@ -342,8 +369,10 @@ module Sentry
342
369
  self.skip_rake_integration = false
343
370
  self.send_client_reports = true
344
371
  self.auto_session_tracking = true
372
+ self.enable_backpressure_handling = false
345
373
  self.trusted_proxies = []
346
374
  self.dsn = ENV['SENTRY_DSN']
375
+ self.spotlight = false
347
376
  self.server_name = server_name_from_env
348
377
  self.instrumenter = :sentry
349
378
  self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL]
@@ -356,6 +385,7 @@ module Sentry
356
385
  self.enable_tracing = nil
357
386
 
358
387
  @transport = Transport::Configuration.new
388
+ @cron = Cron::Configuration.new
359
389
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
360
390
 
361
391
  run_post_initialization_callbacks
@@ -444,14 +474,14 @@ module Sentry
444
474
 
445
475
  def profiles_sample_rate=(profiles_sample_rate)
446
476
  raise ArgumentError, "profiles_sample_rate must be a Numeric or nil" unless is_numeric_or_nil?(profiles_sample_rate)
447
- log_info("Please make sure to include the 'stackprof' gem in your Gemfile to use Profiling with Sentry.") unless defined?(StackProf)
477
+ log_warn("Please make sure to include the 'stackprof' gem in your Gemfile to use Profiling with Sentry.") unless defined?(StackProf)
448
478
  @profiles_sample_rate = profiles_sample_rate
449
479
  end
450
480
 
451
481
  def sending_allowed?
452
482
  @errors = []
453
483
 
454
- valid? && capture_in_environment?
484
+ spotlight || (valid? && capture_in_environment?)
455
485
  end
456
486
 
457
487
  def sample_allowed?
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Cron
5
+ class Configuration
6
+ # Defaults set here will apply to all {Cron::MonitorConfig} objects unless overwritten.
7
+
8
+ # How long (in minutes) after the expected checkin time will we wait
9
+ # until we consider the checkin to have been missed.
10
+ # @return [Integer, nil]
11
+ attr_accessor :default_checkin_margin
12
+
13
+ # How long (in minutes) is the checkin allowed to run for in in_progress
14
+ # before it is considered failed.
15
+ # @return [Integer, nil]
16
+ attr_accessor :default_max_runtime
17
+
18
+ # tz database style timezone string
19
+ # @return [String, nil]
20
+ attr_accessor :default_timezone
21
+ end
22
+ end
23
+ end
@@ -4,7 +4,7 @@ module Sentry
4
4
  MAX_SLUG_LENGTH = 50
5
5
 
6
6
  module Patch
7
- def perform(*args)
7
+ def perform(*args, **opts)
8
8
  slug = self.class.sentry_monitor_slug
9
9
  monitor_config = self.class.sentry_monitor_config
10
10
 
@@ -13,38 +13,49 @@ module Sentry
13
13
  monitor_config: monitor_config)
14
14
 
15
15
  start = Sentry.utc_now.to_i
16
- ret = super
17
- duration = Sentry.utc_now.to_i - start
18
16
 
19
- Sentry.capture_check_in(slug,
20
- :ok,
21
- check_in_id: check_in_id,
22
- duration: duration,
23
- monitor_config: monitor_config)
17
+ begin
18
+ # need to do this on ruby <= 2.6 sadly
19
+ ret = method(:perform).super_method.arity == 0 ? super() : super
20
+ duration = Sentry.utc_now.to_i - start
24
21
 
25
- ret
26
- rescue Exception
27
- duration = Sentry.utc_now.to_i - start
22
+ Sentry.capture_check_in(slug,
23
+ :ok,
24
+ check_in_id: check_in_id,
25
+ duration: duration,
26
+ monitor_config: monitor_config)
28
27
 
29
- Sentry.capture_check_in(slug,
30
- :error,
31
- check_in_id: check_in_id,
32
- duration: duration,
33
- monitor_config: monitor_config)
28
+ ret
29
+ rescue Exception
30
+ duration = Sentry.utc_now.to_i - start
34
31
 
35
- raise
32
+ Sentry.capture_check_in(slug,
33
+ :error,
34
+ check_in_id: check_in_id,
35
+ duration: duration,
36
+ monitor_config: monitor_config)
37
+
38
+ raise
39
+ end
36
40
  end
37
41
  end
38
42
 
39
43
  module ClassMethods
40
44
  def sentry_monitor_check_ins(slug: nil, monitor_config: nil)
45
+ if monitor_config && Sentry.configuration
46
+ cron_config = Sentry.configuration.cron
47
+ monitor_config.checkin_margin ||= cron_config.default_checkin_margin
48
+ monitor_config.max_runtime ||= cron_config.default_max_runtime
49
+ monitor_config.timezone ||= cron_config.default_timezone
50
+ end
51
+
41
52
  @sentry_monitor_slug = slug
42
53
  @sentry_monitor_config = monitor_config
43
54
 
44
55
  prepend Patch
45
56
  end
46
57
 
47
- def sentry_monitor_slug
58
+ def sentry_monitor_slug(name: self.name)
48
59
  @sentry_monitor_slug ||= begin
49
60
  slug = name.gsub('::', '-').downcase
50
61
  slug[-MAX_SLUG_LENGTH..-1] || slug
@@ -56,8 +67,6 @@ module Sentry
56
67
  end
57
68
  end
58
69
 
59
- extend ClassMethods
60
-
61
70
  def self.included(base)
62
71
  base.extend(ClassMethods)
63
72
  end
data/lib/sentry/event.rb CHANGED
@@ -76,34 +76,6 @@ module Sentry
76
76
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
77
77
  end
78
78
 
79
- class << self
80
- # @!visibility private
81
- def get_log_message(event_hash)
82
- message = event_hash[:message] || event_hash['message']
83
-
84
- return message unless message.nil? || message.empty?
85
-
86
- message = get_message_from_exception(event_hash)
87
-
88
- return message unless message.nil? || message.empty?
89
-
90
- message = event_hash[:transaction] || event_hash["transaction"]
91
-
92
- return message unless message.nil? || message.empty?
93
-
94
- '<no message value>'
95
- end
96
-
97
- # @!visibility private
98
- def get_message_from_exception(event_hash)
99
- if exception = event_hash.dig(:exception, :values, 0)
100
- "#{exception[:type]}: #{exception[:value]}"
101
- elsif exception = event_hash.dig("exception", "values", 0)
102
- "#{exception["type"]}: #{exception["value"]}"
103
- end
104
- end
105
- end
106
-
107
79
  # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
108
80
  # @return [Configuration]
109
81
  def configuration
data/lib/sentry/hub.rb CHANGED
@@ -156,7 +156,7 @@ module Sentry
156
156
  capture_event(event, **options, &block)
157
157
  end
158
158
 
159
- def capture_check_in(slug, status, **options, &block)
159
+ def capture_check_in(slug, status, **options)
160
160
  check_argument_type!(slug, ::String)
161
161
  check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES)
162
162
 
@@ -176,7 +176,7 @@ module Sentry
176
176
 
177
177
  return unless event
178
178
 
179
- capture_event(event, **options, &block)
179
+ capture_event(event, **options)
180
180
  event.check_in_id
181
181
  end
182
182
 
data/lib/sentry/rake.rb CHANGED
@@ -17,15 +17,6 @@ module Sentry
17
17
  super
18
18
  end
19
19
  end
20
-
21
- module Task
22
- # @api private
23
- def execute(args=nil)
24
- return super unless Sentry.initialized? && Sentry.get_current_hub
25
-
26
- super
27
- end
28
- end
29
20
  end
30
21
  end
31
22
 
@@ -34,8 +25,4 @@ module Rake
34
25
  class Application
35
26
  prepend(Sentry::Rake::Application)
36
27
  end
37
-
38
- class Task
39
- prepend(Sentry::Rake::Task)
40
- end
41
28
  end
data/lib/sentry/scope.rb CHANGED
@@ -44,12 +44,18 @@ module Sentry
44
44
  # @param hint [Hash] the hint data that'll be passed to event processors.
45
45
  # @return [Event]
46
46
  def apply_to_event(event, hint = nil)
47
- event.tags = tags.merge(event.tags)
48
- event.user = user.merge(event.user)
49
- event.extra = extra.merge(event.extra)
50
- event.contexts = contexts.merge(event.contexts)
51
- event.transaction = transaction_name if transaction_name
52
- event.transaction_info = { source: transaction_source } if transaction_source
47
+ unless event.is_a?(CheckInEvent)
48
+ event.tags = tags.merge(event.tags)
49
+ event.user = user.merge(event.user)
50
+ event.extra = extra.merge(event.extra)
51
+ event.contexts = contexts.merge(event.contexts)
52
+ event.transaction = transaction_name if transaction_name
53
+ event.transaction_info = { source: transaction_source } if transaction_source
54
+ event.fingerprint = fingerprint
55
+ event.level = level
56
+ event.breadcrumbs = breadcrumbs
57
+ event.rack_env = rack_env if rack_env
58
+ end
53
59
 
54
60
  if span
55
61
  event.contexts[:trace] ||= span.get_trace_context
@@ -58,11 +64,6 @@ module Sentry
58
64
  event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
59
65
  end
60
66
 
61
- event.fingerprint = fingerprint
62
- event.level = level
63
- event.breadcrumbs = breadcrumbs
64
- event.rack_env = rack_env if rack_env
65
-
66
67
  all_event_processors = self.class.global_event_processors + @event_processors
67
68
 
68
69
  unless all_event_processors.empty?
@@ -218,7 +218,12 @@ module Sentry
218
218
  if sample_rate == true
219
219
  @sampled = true
220
220
  else
221
- @sampled = Random.rand < sample_rate
221
+ if Sentry.backpressure_monitor
222
+ factor = Sentry.backpressure_monitor.downsample_factor
223
+ @effective_sample_rate /= 2**factor
224
+ end
225
+
226
+ @sampled = Random.rand < @effective_sample_rate
222
227
  end
223
228
 
224
229
  if @sampled
@@ -257,7 +262,9 @@ module Sentry
257
262
  event = hub.current_client.event_from_transaction(self)
258
263
  hub.capture_event(event)
259
264
  else
260
- hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
265
+ is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive?
266
+ reason = is_backpressure ? :backpressure : :sample_rate
267
+ hub.current_client.transport.record_lost_event(reason, 'transaction')
261
268
  end
262
269
  end
263
270
 
@@ -14,11 +14,19 @@ module Sentry
14
14
  RATE_LIMIT_HEADER = "x-sentry-rate-limits"
15
15
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
16
16
 
17
+ # The list of errors ::Net::HTTP is known to raise
18
+ # See https://github.com/ruby/ruby/blob/b0c639f249165d759596f9579fa985cb30533de6/lib/bundler/fetcher.rb#L281-L286
19
+ HTTP_ERRORS = [
20
+ Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH,
21
+ Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
22
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
23
+ Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
24
+ ].freeze
25
+
26
+
17
27
  def initialize(*args)
18
28
  super
19
- @endpoint = @dsn.envelope_endpoint
20
-
21
- log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
29
+ log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
22
30
  end
23
31
 
24
32
  def send_data(data)
@@ -32,34 +40,76 @@ module Sentry
32
40
  headers = {
33
41
  'Content-Type' => CONTENT_TYPE,
34
42
  'Content-Encoding' => encoding,
35
- 'X-Sentry-Auth' => generate_auth_header,
36
43
  'User-Agent' => USER_AGENT
37
44
  }
38
45
 
46
+ auth_header = generate_auth_header
47
+ headers['X-Sentry-Auth'] = auth_header if auth_header
48
+
39
49
  response = conn.start do |http|
40
- request = ::Net::HTTP::Post.new(@endpoint, headers)
50
+ request = ::Net::HTTP::Post.new(endpoint, headers)
41
51
  request.body = data
42
52
  http.request(request)
43
53
  end
44
54
 
45
55
  if response.code.match?(/\A2\d{2}/)
46
- if has_rate_limited_header?(response)
47
- handle_rate_limited_response(response)
48
- end
56
+ handle_rate_limited_response(response) if has_rate_limited_header?(response)
57
+ elsif response.code == "429"
58
+ log_debug("the server responded with status 429")
59
+ handle_rate_limited_response(response)
49
60
  else
50
61
  error_info = "the server responded with status #{response.code}"
62
+ error_info += "\nbody: #{response.body}"
63
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
64
+
65
+ raise Sentry::ExternalError, error_info
66
+ end
67
+ rescue SocketError, *HTTP_ERRORS => e
68
+ on_error if respond_to?(:on_error)
69
+ raise Sentry::ExternalError.new(e&.message)
70
+ end
71
+
72
+ def endpoint
73
+ @dsn.envelope_endpoint
74
+ end
75
+
76
+ def generate_auth_header
77
+ return nil unless @dsn
78
+
79
+ now = Sentry.utc_now.to_i
80
+ fields = {
81
+ 'sentry_version' => PROTOCOL_VERSION,
82
+ 'sentry_client' => USER_AGENT,
83
+ 'sentry_timestamp' => now,
84
+ 'sentry_key' => @dsn.public_key
85
+ }
86
+ fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
87
+ 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
88
+ end
51
89
 
52
- if response.code == "429"
53
- handle_rate_limited_response(response)
90
+ def conn
91
+ server = URI(@dsn.server)
92
+
93
+ # connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
94
+ # Net::HTTP will automatically read the env vars.
95
+ # See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
96
+ connection =
97
+ if proxy = normalize_proxy(@transport_configuration.proxy)
98
+ ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
54
99
  else
55
- error_info += "\nbody: #{response.body}"
56
- error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
100
+ ::Net::HTTP.new(server.hostname, server.port)
57
101
  end
58
102
 
59
- raise Sentry::ExternalError, error_info
103
+ connection.use_ssl = server.scheme == "https"
104
+ connection.read_timeout = @transport_configuration.timeout
105
+ connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
106
+ connection.open_timeout = @transport_configuration.open_timeout
107
+
108
+ ssl_configuration.each do |key, value|
109
+ connection.send("#{key}=", value)
60
110
  end
61
- rescue SocketError => e
62
- raise Sentry::ExternalError.new(e.message)
111
+
112
+ connection
63
113
  end
64
114
 
65
115
  private
@@ -126,31 +176,6 @@ module Sentry
126
176
  @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
127
177
  end
128
178
 
129
- def conn
130
- server = URI(@dsn.server)
131
-
132
- # connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
133
- # Net::HTTP will automatically read the env vars.
134
- # See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
135
- connection =
136
- if proxy = normalize_proxy(@transport_configuration.proxy)
137
- ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
138
- else
139
- ::Net::HTTP.new(server.hostname, server.port)
140
- end
141
-
142
- connection.use_ssl = server.scheme == "https"
143
- connection.read_timeout = @transport_configuration.timeout
144
- connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
145
- connection.open_timeout = @transport_configuration.open_timeout
146
-
147
- ssl_configuration.each do |key, value|
148
- connection.send("#{key}=", value)
149
- end
150
-
151
- connection
152
- end
153
-
154
179
  # @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`.
155
180
  # Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys.
156
181
  # @return [Hash] Normalized proxy config that will be passed into `Net::HTTP`
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
5
+
6
+ module Sentry
7
+ # Designed to just report events to Spotlight in development.
8
+ class SpotlightTransport < HTTPTransport
9
+ DEFAULT_SIDECAR_URL = "http://localhost:8969/stream"
10
+ MAX_FAILED_REQUESTS = 3
11
+
12
+ def initialize(configuration)
13
+ super
14
+ @sidecar_url = configuration.spotlight.is_a?(String) ? configuration.spotlight : DEFAULT_SIDECAR_URL
15
+ @failed = 0
16
+ @logged = false
17
+
18
+ log_debug("[Spotlight] initialized for url #{@sidecar_url}")
19
+ end
20
+
21
+ def endpoint
22
+ "/stream"
23
+ end
24
+
25
+ def send_data(data)
26
+ if @failed >= MAX_FAILED_REQUESTS
27
+ unless @logged
28
+ log_debug("[Spotlight] disabling because of too many request failures")
29
+ @logged = true
30
+ end
31
+
32
+ return
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def on_error
39
+ @failed += 1
40
+ end
41
+
42
+ # Similar to HTTPTransport connection, but does not support Proxy and SSL
43
+ def conn
44
+ sidecar = URI(@sidecar_url)
45
+ connection = ::Net::HTTP.new(sidecar.hostname, sidecar.port, nil)
46
+ connection.use_ssl = false
47
+ connection
48
+ end
49
+ end
50
+ end
@@ -19,7 +19,8 @@ module Sentry
19
19
  :sample_rate,
20
20
  :before_send,
21
21
  :event_processor,
22
- :insufficient_data
22
+ :insufficient_data,
23
+ :backpressure
23
24
  ]
24
25
 
25
26
  include LoggingHelper
@@ -74,7 +75,7 @@ module Sentry
74
75
  result, oversized = item.serialize
75
76
 
76
77
  if oversized
77
- log_info("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
78
+ log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}")
78
79
 
79
80
  next
80
81
  end
@@ -119,16 +120,8 @@ module Sentry
119
120
  !!delay && delay > Time.now
120
121
  end
121
122
 
122
- def generate_auth_header
123
- now = Sentry.utc_now.to_i
124
- fields = {
125
- 'sentry_version' => PROTOCOL_VERSION,
126
- 'sentry_client' => USER_AGENT,
127
- 'sentry_timestamp' => now,
128
- 'sentry_key' => @dsn.public_key
129
- }
130
- fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
131
- 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
123
+ def any_rate_limited?
124
+ @rate_limits.values.any? { |t| t && t > Time.now }
132
125
  end
133
126
 
134
127
  def envelope_from_event(event)
@@ -175,11 +168,20 @@ module Sentry
175
168
  @discarded_events[[reason, item_type]] += 1
176
169
  end
177
170
 
171
+ def flush
172
+ client_report_headers, client_report_payload = fetch_pending_client_report(force: true)
173
+ return unless client_report_headers
174
+
175
+ envelope = Envelope.new
176
+ envelope.add_item(client_report_headers, client_report_payload)
177
+ send_envelope(envelope)
178
+ end
179
+
178
180
  private
179
181
 
180
- def fetch_pending_client_report
182
+ def fetch_pending_client_report(force: false)
181
183
  return nil unless @send_client_reports
182
- return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
184
+ return nil if !force && @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
183
185
  return nil if @discarded_events.empty?
184
186
 
185
187
  discarded_events_hash = @discarded_events.map do |key, val|
@@ -206,7 +208,7 @@ module Sentry
206
208
  def reject_rate_limited_items(envelope)
207
209
  envelope.items.reject! do |item|
208
210
  if is_rate_limited?(item.type)
209
- log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
211
+ log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
210
212
  record_lost_event(:ratelimit_backoff, item.type)
211
213
 
212
214
  true
@@ -220,3 +222,4 @@ end
220
222
 
221
223
  require "sentry/transport/dummy_transport"
222
224
  require "sentry/transport/http_transport"
225
+ require "sentry/transport/spotlight_transport"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.14.0"
4
+ VERSION = "5.16.1"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -21,6 +21,7 @@ require "sentry/transaction"
21
21
  require "sentry/hub"
22
22
  require "sentry/background_worker"
23
23
  require "sentry/session_flusher"
24
+ require "sentry/backpressure_monitor"
24
25
  require "sentry/cron/monitor_check_ins"
25
26
 
26
27
  [
@@ -65,13 +66,17 @@ module Sentry
65
66
  end
66
67
 
67
68
  # @!attribute [rw] background_worker
68
- # @return [BackgroundWorker, nil]
69
+ # @return [BackgroundWorker]
69
70
  attr_accessor :background_worker
70
71
 
71
72
  # @!attribute [r] session_flusher
72
73
  # @return [SessionFlusher, nil]
73
74
  attr_reader :session_flusher
74
75
 
76
+ # @!attribute [r] backpressure_monitor
77
+ # @return [BackpressureMonitor, nil]
78
+ attr_reader :backpressure_monitor
79
+
75
80
  ##### Patch Registration #####
76
81
 
77
82
  # @!visibility private
@@ -217,17 +222,9 @@ module Sentry
217
222
  Thread.current.thread_variable_set(THREAD_LOCAL, hub)
218
223
  @main_hub = hub
219
224
  @background_worker = Sentry::BackgroundWorker.new(config)
220
-
221
- @session_flusher = if config.auto_session_tracking
222
- Sentry::SessionFlusher.new(config, client)
223
- else
224
- nil
225
- end
226
-
227
- if config.include_local_variables
228
- exception_locals_tp.enable
229
- end
230
-
225
+ @session_flusher = config.auto_session_tracking ? Sentry::SessionFlusher.new(config, client) : nil
226
+ @backpressure_monitor = config.enable_backpressure_handling ? Sentry::BackpressureMonitor.new(config, client) : nil
227
+ exception_locals_tp.enable if config.include_local_variables
231
228
  at_exit { close }
232
229
  end
233
230
 
@@ -236,20 +233,27 @@ module Sentry
236
233
  #
237
234
  # @return [void]
238
235
  def close
239
- if @background_worker
240
- @background_worker.shutdown
241
- @background_worker = nil
242
- end
243
-
244
236
  if @session_flusher
237
+ @session_flusher.flush
245
238
  @session_flusher.kill
246
239
  @session_flusher = nil
247
240
  end
248
241
 
249
- if configuration&.include_local_variables
250
- exception_locals_tp.disable
242
+ if @backpressure_monitor
243
+ @backpressure_monitor.kill
244
+ @backpressure_monitor = nil
251
245
  end
252
246
 
247
+ if client = get_current_client
248
+ client.transport.flush
249
+
250
+ if client.configuration.include_local_variables
251
+ exception_locals_tp.disable
252
+ end
253
+ end
254
+
255
+ @background_worker.shutdown
256
+
253
257
  @main_hub = nil
254
258
  Thread.current.thread_variable_set(THREAD_LOCAL, nil)
255
259
  end
@@ -442,12 +446,10 @@ module Sentry
442
446
  # @option options [Integer] duration seconds elapsed since this monitor started
443
447
  # @option options [Cron::MonitorConfig] monitor_config configuration for this monitor
444
448
  #
445
- # @yieldparam scope [Scope]
446
- #
447
449
  # @return [String, nil] The {CheckInEvent#check_in_id} to use for later updates on the same slug
448
- def capture_check_in(slug, status, **options, &block)
450
+ def capture_check_in(slug, status, **options)
449
451
  return unless initialized?
450
- get_current_hub.capture_check_in(slug, status, **options, &block)
452
+ get_current_hub.capture_check_in(slug, status, **options)
451
453
  end
452
454
 
453
455
  # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.14.0
4
+ version: 5.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-27 00:00:00.000000000 Z
11
+ date: 2024-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -51,6 +51,7 @@ files:
51
51
  - bin/setup
52
52
  - lib/sentry-ruby.rb
53
53
  - lib/sentry/background_worker.rb
54
+ - lib/sentry/backpressure_monitor.rb
54
55
  - lib/sentry/backtrace.rb
55
56
  - lib/sentry/baggage.rb
56
57
  - lib/sentry/breadcrumb.rb
@@ -61,6 +62,7 @@ files:
61
62
  - lib/sentry/configuration.rb
62
63
  - lib/sentry/core_ext/object/deep_dup.rb
63
64
  - lib/sentry/core_ext/object/duplicable.rb
65
+ - lib/sentry/cron/configuration.rb
64
66
  - lib/sentry/cron/monitor_check_ins.rb
65
67
  - lib/sentry/cron/monitor_config.rb
66
68
  - lib/sentry/cron/monitor_schedule.rb
@@ -100,6 +102,7 @@ files:
100
102
  - lib/sentry/transport/configuration.rb
101
103
  - lib/sentry/transport/dummy_transport.rb
102
104
  - lib/sentry/transport/http_transport.rb
105
+ - lib/sentry/transport/spotlight_transport.rb
103
106
  - lib/sentry/utils/argument_checking_helper.rb
104
107
  - lib/sentry/utils/custom_inspection.rb
105
108
  - lib/sentry/utils/encoding_helper.rb