sentry-ruby 5.14.0 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
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