sentry-ruby-core 4.3.2 → 4.4.0.pre.beta.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 85c558cf77534e3d3fea7d30e417171302f485042b8a09b3a1114c714f1b17ec
4
- data.tar.gz: d8077c3cfee56f1427d03770dc8d28e9920bbba4d97e14e983d3b7a5a949be95
3
+ metadata.gz: cb17ca62057f29a255a7d725bd574775d840e987cbc3c067a9294b63311d26aa
4
+ data.tar.gz: 9268967f80f8406bbcf3bf2b0dc150567b3adc16d1c4b38fd8ef5df5a1559737
5
5
  SHA512:
6
- metadata.gz: 9cc8fa4030f678d6b2cd34d1c5752e4da6065527ed598e6f43a14ab6adcb44fbe4f104b99de9dd65d45995f726fc074bf3944891348ce4491a385ce22acfa493
7
- data.tar.gz: 8fc20069fbddcb6dba3206ea8d7578c9da54f5a0812c0388e9e1afc983befcaaaa3d32390af78b6f3bee6294e997455dbcc807d7543380bc20740b314534dd7b
6
+ metadata.gz: 3d57f757d5d7c36c24bf296fbb673c8afe20fb3fadc89c1a1f4046856188286f9db9c7fdd2b6c3d49e921a302f179f1c4acef4ac737b802cc0640e979eab0de8
7
+ data.tar.gz: e5900027bce05c31a9bd10ea6fb2f660deee1494e37545a0637ea89767353add86b39fc4dde07de0c4ac9fcadb2f2d2bc8441308d4f89396e5c5de124ed7f445
data/.craft.yml CHANGED
@@ -13,9 +13,6 @@ targets:
13
13
  # we always need to make sure sentry-ruby-core is present when pushing to any target
14
14
  - name: gem
15
15
  onlyIfPresent: /^sentry-ruby-core-\d.*\.gem$/
16
- - name: github
17
- onlyIfPresent: /^sentry-ruby-core-\d.*\.gem$/
18
- tagPrefix: sentry-ruby-v
19
16
  - name: registry
20
17
  onlyIfPresent: /^sentry-ruby-core-\d.*\.gem$/
21
18
  type: sdk
@@ -26,3 +23,7 @@ targets:
26
23
  type: sdk
27
24
  config:
28
25
  canonical: 'gem:sentry-ruby-core'
26
+ - name: github
27
+ onlyIfPresent: /^sentry-ruby-core-\d.*\.gem$/
28
+ tagPrefix: sentry-ruby-v
29
+ changelog: sentry-ruby/CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.4.0-beta.0
4
+
5
+ ### Features
6
+
7
+ #### Support category-based rate limiting [#1336](https://github.com/getsentry/sentry-ruby/pull/1336)
8
+
9
+ Sentry rate limits different types of events. And when rate limiting is enabled, it sends back a `429` response to the SDK. Currently, the SDK would then raise an error like this:
10
+
11
+ ```
12
+ Unable to record event with remote Sentry server (Sentry::Error - the server responded with status 429
13
+ body: {"detail":"event rejected due to rate limit"}):
14
+ ```
15
+
16
+ This change improves the SDK's handling on such responses by:
17
+
18
+ - Not treating them as errors, so you don't see the noise anymore.
19
+ - Halting event sending for a while according to the duration provided in the response. And warns you with a message like:
20
+
21
+ ```
22
+ Envelope [event] not sent: Excluded by random sample
23
+ ```
24
+
25
+ #### Record request span from Net::HTTP library [#1381](https://github.com/getsentry/sentry-ruby/pull/1381)
26
+
27
+ Now any outgoing requests will be recorded as a tracing span. Example:
28
+
29
+ <img width="60%" alt="net:http span example" src="https://user-images.githubusercontent.com/5079556/115838944-c1279a80-a44c-11eb-8c67-dfd92bf68bbd.png">
30
+
31
+
32
+ #### Record breadcrumb for Net::HTTP requests [#1394](https://github.com/getsentry/sentry-ruby/pull/1394)
33
+
34
+ With the new `http_logger` breadcrumbs logger:
35
+
36
+ ```ruby
37
+ config.breadcrumbs_logger = [:http_logger]
38
+ ```
39
+
40
+ The SDK now records a new `net.http` breadcrumb whenever the user makes a request with the `Net::HTTP` library.
41
+
42
+ <img width="60%" alt="net http breadcrumb" src="https://user-images.githubusercontent.com/5079556/114298326-5f7c3d80-9ae8-11eb-9108-222384a7f1a2.png">
43
+
44
+ #### Support config.debug configuration option [#1400](https://github.com/getsentry/sentry-ruby/pull/1400)
45
+
46
+ It'll determine whether the SDK should run in the debugging mode. Default is `false`. When set to true, SDK errors will be logged with backtrace.
47
+
48
+ #### Add the third tracing state [#1402](https://github.com/getsentry/sentry-ruby/pull/1402)
49
+ - `rate == 0` - Tracing enabled. Rejects all locally created transactions but respects sentry-trace.
50
+ - `1 > rate > 0` - Tracing enabled. Samples locally created transactions with the rate and respects sentry-trace.
51
+ - `rate < 0` or `rate > 1` - Tracing disabled.
52
+
53
+ ### Refactorings
54
+
55
+ - Let Transaction constructor take an optional hub argument [#1384](https://github.com/getsentry/sentry-ruby/pull/1384)
56
+ - Introduce LoggingHelper [#1385](https://github.com/getsentry/sentry-ruby/pull/1385)
57
+ - Raise exception if a Transaction is initialized without a hub [#1391](https://github.com/getsentry/sentry-ruby/pull/1391)
58
+ - Make hub a required argument for Transaction constructor [#1401](https://github.com/getsentry/sentry-ruby/pull/1401)
59
+
60
+ ### Bug Fixes
61
+
62
+ - Check `Scope#set_context`'s value argument [#1415](https://github.com/getsentry/sentry-ruby/pull/1415)
63
+
3
64
  ## 4.3.2
4
65
 
5
66
  - Correct type attribute's usages [#1354](https://github.com/getsentry/sentry-ruby/pull/1354)
data/Gemfile CHANGED
@@ -9,7 +9,10 @@ gem "i18n", "<= 1.8.7"
9
9
  gem "rake", "~> 12.0"
10
10
  gem "rspec", "~> 3.0"
11
11
  gem "rspec-retry"
12
+ gem "webmock"
13
+ gem "timecop"
12
14
  gem "codecov", "0.2.12"
15
+ gem "tapping_device"
13
16
 
14
17
  gem "pry"
15
18
  gem "rack" unless ENV["WITHOUT_RACK"] == "1"
data/lib/sentry-ruby.rb CHANGED
@@ -6,6 +6,8 @@ require "sentry/version"
6
6
  require "sentry/exceptions"
7
7
  require "sentry/core_ext/object/deep_dup"
8
8
  require "sentry/utils/argument_checking_helper"
9
+ require "sentry/utils/logging_helper"
10
+ require "sentry/net/http"
9
11
  require "sentry/configuration"
10
12
  require "sentry/logger"
11
13
  require "sentry/event"
@@ -4,21 +4,24 @@ require "concurrent/configuration"
4
4
 
5
5
  module Sentry
6
6
  class BackgroundWorker
7
- attr_reader :max_queue, :number_of_threads
7
+ include LoggingHelper
8
+
9
+ attr_reader :max_queue, :number_of_threads, :logger
8
10
 
9
11
  def initialize(configuration)
10
12
  @max_queue = 30
11
13
  @number_of_threads = configuration.background_worker_threads
14
+ @logger = configuration.logger
12
15
 
13
16
  @executor =
14
17
  if configuration.async
15
- configuration.logger.debug(LOGGER_PROGNAME) { "config.async is set, BackgroundWorker is disabled" }
18
+ log_debug("config.async is set, BackgroundWorker is disabled")
16
19
  Concurrent::ImmediateExecutor.new
17
20
  elsif @number_of_threads == 0
18
- configuration.logger.debug(LOGGER_PROGNAME) { "config.background_worker_threads is set to 0, all events will be sent synchronously" }
21
+ log_debug("config.background_worker_threads is set to 0, all events will be sent synchronously")
19
22
  Concurrent::ImmediateExecutor.new
20
23
  else
21
- configuration.logger.debug(LOGGER_PROGNAME) { "initialized a background worker with #{@number_of_threads} threads" }
24
+ log_debug("initialized a background worker with #{@number_of_threads} threads")
22
25
 
23
26
  Concurrent::ThreadPoolExecutor.new(
24
27
  min_threads: 0,
data/lib/sentry/client.rb CHANGED
@@ -2,6 +2,8 @@ require "sentry/transport"
2
2
 
3
3
  module Sentry
4
4
  class Client
5
+ include LoggingHelper
6
+
5
7
  attr_reader :transport, :configuration, :logger
6
8
 
7
9
  def initialize(configuration)
@@ -36,7 +38,7 @@ module Sentry
36
38
 
37
39
  event
38
40
  rescue => e
39
- logger.error(LOGGER_PROGNAME) { "Event capturing failed: #{e.message}" }
41
+ log_error("Event capturing failed", e, debug: configuration.debug)
40
42
  nil
41
43
  end
42
44
 
@@ -76,7 +78,7 @@ module Sentry
76
78
  event = configuration.before_send.call(event, hint)
77
79
 
78
80
  if event.nil?
79
- logger.info(LOGGER_PROGNAME) { "Discarded event because before_send returned nil" }
81
+ log_info("Discarded event because before_send returned nil")
80
82
  return
81
83
  end
82
84
  end
@@ -86,8 +88,10 @@ module Sentry
86
88
  event
87
89
  rescue => e
88
90
  loggable_event_type = (event_type || "event").capitalize
89
- logger.error(LOGGER_PROGNAME) { "#{loggable_event_type} sending failed: #{e.message}" }
90
- logger.error(LOGGER_PROGNAME) { "Unreported #{loggable_event_type}: #{Event.get_log_message(event.to_hash)}" }
91
+ log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
92
+
93
+ event_info = Event.get_log_message(event.to_hash)
94
+ log_info("Unreported #{loggable_event_type}: #{event_info}")
91
95
  raise
92
96
  end
93
97
 
@@ -112,9 +116,8 @@ module Sentry
112
116
  end
113
117
  rescue => e
114
118
  loggable_event_type = event_hash["type"] || "event"
115
- logger.error(LOGGER_PROGNAME) { "Async #{loggable_event_type} sending failed: #{e.message}" }
119
+ log_error("Async #{loggable_event_type} sending failed", e, debug: configuration.debug)
116
120
  send_event(event, hint)
117
121
  end
118
-
119
122
  end
120
123
  end
@@ -8,6 +8,7 @@ require "sentry/interfaces/stacktrace_builder"
8
8
 
9
9
  module Sentry
10
10
  class Configuration
11
+ include LoggingHelper
11
12
  # Directories to be recognized as part of your app. e.g. if you
12
13
  # have an `engines` dir at the root of your project, you may want
13
14
  # to set this to something like /(app|config|engines|lib)/
@@ -71,6 +72,10 @@ module Sentry
71
72
  # RACK_ENV by default.
72
73
  attr_reader :environment
73
74
 
75
+ # Whether the SDK should run in the debugging mode. Default is false.
76
+ # If set to true, SDK errors will be logged with backtrace
77
+ attr_accessor :debug
78
+
74
79
  # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
75
80
  attr_reader :dsn
76
81
 
@@ -167,13 +172,12 @@ module Sentry
167
172
  LOG_PREFIX = "** [Sentry] ".freeze
168
173
  MODULE_SEPARATOR = "::".freeze
169
174
 
170
- AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
171
-
172
175
  # Post initialization callbacks are called at the end of initialization process
173
176
  # allowing extending the configuration of sentry-ruby by multiple extensions
174
177
  @@post_initialization_callbacks = []
175
178
 
176
179
  def initialize
180
+ self.debug = false
177
181
  self.background_worker_threads = Concurrent.processor_count
178
182
  self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
179
183
  self.breadcrumbs_logger = []
@@ -226,10 +230,6 @@ module Sentry
226
230
  if logger.is_a?(Array)
227
231
  logger
228
232
  else
229
- unless AVAILABLE_BREADCRUMBS_LOGGERS.include?(logger)
230
- raise Sentry::Error, "Unsupported breadcrumbs logger. Supported loggers: #{AVAILABLE_BREADCRUMBS_LOGGERS}"
231
- end
232
-
233
233
  Array(logger)
234
234
  end
235
235
 
@@ -278,10 +278,10 @@ module Sentry
278
278
  def exception_class_allowed?(exc)
279
279
  if exc.is_a?(Sentry::Error)
280
280
  # Try to prevent error reporting loops
281
- logger.debug(LOGGER_PROGNAME) { "Refusing to capture Sentry error: #{exc.inspect}" }
281
+ log_debug("Refusing to capture Sentry error: #{exc.inspect}")
282
282
  false
283
283
  elsif excluded_exception?(exc)
284
- logger.debug(LOGGER_PROGNAME) { "User excluded error: #{exc.inspect}" }
284
+ log_debug("User excluded error: #{exc.inspect}")
285
285
  false
286
286
  else
287
287
  true
@@ -293,7 +293,7 @@ module Sentry
293
293
  end
294
294
 
295
295
  def tracing_enabled?
296
- !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
296
+ !!((@traces_sample_rate && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler)
297
297
  end
298
298
 
299
299
  def stacktrace_builder
@@ -314,7 +314,7 @@ module Sentry
314
314
  detect_release_from_capistrano ||
315
315
  detect_release_from_heroku
316
316
  rescue => e
317
- logger.error(LOGGER_PROGNAME) { "Error detecting release: #{e.message}" }
317
+ log_error("Error detecting release", e, debug: debug)
318
318
  end
319
319
 
320
320
  def excluded_exception?(incoming_exception)
@@ -349,7 +349,7 @@ module Sentry
349
349
  def detect_release_from_heroku
350
350
  return unless running_on_heroku?
351
351
  return if ENV['CI']
352
- logger.warn(LOGGER_PROGNAME) { HEROKU_DYNO_METADATA_MESSAGE } && return unless ENV['HEROKU_SLUG_COMMIT']
352
+ log_warn(HEROKU_DYNO_METADATA_MESSAGE) && return unless ENV['HEROKU_SLUG_COMMIT']
353
353
 
354
354
  ENV['HEROKU_SLUG_COMMIT']
355
355
  end
data/lib/sentry/hub.rb CHANGED
@@ -21,6 +21,10 @@ module Sentry
21
21
  current_layer&.client
22
22
  end
23
23
 
24
+ def configuration
25
+ current_client.configuration
26
+ end
27
+
24
28
  def current_scope
25
29
  current_layer&.scope
26
30
  end
@@ -69,10 +73,10 @@ module Sentry
69
73
  @stack.pop
70
74
  end
71
75
 
72
- def start_transaction(transaction: nil, configuration: Sentry.configuration, custom_sampling_context: {}, **options)
76
+ def start_transaction(transaction: nil, custom_sampling_context: {}, **options)
73
77
  return unless configuration.tracing_enabled?
74
78
 
75
- transaction ||= Transaction.new(**options)
79
+ transaction ||= Transaction.new(**options.merge(hub: self))
76
80
 
77
81
  sampling_context = {
78
82
  transaction_context: transaction.to_hash,
@@ -81,7 +85,7 @@ module Sentry
81
85
 
82
86
  sampling_context.merge!(custom_sampling_context)
83
87
 
84
- transaction.set_initial_sample_decision(configuration: current_client.configuration, sampling_context: sampling_context)
88
+ transaction.set_initial_sample_decision(sampling_context: sampling_context)
85
89
  transaction
86
90
  end
87
91
 
@@ -0,0 +1,87 @@
1
+ require "net/http"
2
+
3
+ module Sentry
4
+ module Net
5
+ module HTTP
6
+ OP_NAME = "net.http"
7
+
8
+ def request(req, body = nil, &block)
9
+ super.tap do |res|
10
+ record_sentry_breadcrumb(req, res)
11
+ record_sentry_span(req, res)
12
+ end
13
+ end
14
+
15
+ def do_start
16
+ super.tap do
17
+ start_sentry_span
18
+ end
19
+ end
20
+
21
+ def do_finish
22
+ super.tap do
23
+ finish_sentry_span
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def record_sentry_breadcrumb(req, res)
30
+ if Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
31
+ return if from_sentry_sdk?
32
+
33
+ request_info = extract_request_info(req)
34
+ crumb = Sentry::Breadcrumb.new(
35
+ level: :info,
36
+ category: OP_NAME,
37
+ type: :info,
38
+ data: {
39
+ method: request_info[:method],
40
+ url: request_info[:url],
41
+ status: res.code.to_i
42
+ }
43
+ )
44
+ Sentry.add_breadcrumb(crumb)
45
+ end
46
+ end
47
+
48
+ def record_sentry_span(req, res)
49
+ if Sentry.initialized? && @sentry_span
50
+ request_info = extract_request_info(req)
51
+ @sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
52
+ @sentry_span.set_data(:status, res.code.to_i)
53
+ end
54
+ end
55
+
56
+ def start_sentry_span
57
+ if Sentry.initialized? && transaction = Sentry.get_current_scope.get_transaction
58
+ return if from_sentry_sdk?
59
+ return if transaction.sampled == false
60
+
61
+ child_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
62
+ @sentry_span = child_span
63
+ end
64
+ end
65
+
66
+ def finish_sentry_span
67
+ if Sentry.initialized? && @sentry_span
68
+ @sentry_span.set_timestamp(Sentry.utc_now.to_f)
69
+ @sentry_span = nil
70
+ end
71
+ end
72
+
73
+ def from_sentry_sdk?
74
+ dsn_host = Sentry.configuration.dsn.host
75
+ dsn_host == self.address
76
+ end
77
+
78
+ def extract_request_info(req)
79
+ uri = req.uri
80
+ url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
81
+ { method: req.method, url: url }
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ Net::HTTP.send(:prepend, Sentry::Net::HTTP)
data/lib/sentry/scope.rb CHANGED
@@ -130,6 +130,7 @@ module Sentry
130
130
  end
131
131
 
132
132
  def set_context(key, value)
133
+ check_argument_type!(value, Hash)
133
134
  @contexts.merge!(key => value)
134
135
  end
135
136
 
data/lib/sentry/span.rb CHANGED
@@ -21,7 +21,16 @@ module Sentry
21
21
  attr_reader :trace_id, :span_id, :parent_span_id, :sampled, :start_timestamp, :timestamp, :description, :op, :status, :tags, :data
22
22
  attr_accessor :span_recorder, :transaction
23
23
 
24
- def initialize(description: nil, op: nil, status: nil, trace_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil)
24
+ def initialize(
25
+ description: nil,
26
+ op: nil,
27
+ status: nil,
28
+ trace_id: nil,
29
+ parent_span_id: nil,
30
+ sampled: nil,
31
+ start_timestamp: nil,
32
+ timestamp: nil
33
+ )
25
34
  @trace_id = trace_id || SecureRandom.uuid.delete("-")
26
35
  @span_id = SecureRandom.hex(8)
27
36
  @parent_span_id = parent_span_id
@@ -10,19 +10,24 @@ module Sentry
10
10
  UNLABELD_NAME = "<unlabeled transaction>".freeze
11
11
  MESSAGE_PREFIX = "[Tracing]"
12
12
 
13
- attr_reader :name, :parent_sampled
13
+ include LoggingHelper
14
14
 
15
- def initialize(name: nil, parent_sampled: nil, **options)
15
+ attr_reader :name, :parent_sampled, :hub, :configuration, :logger
16
+
17
+ def initialize(name: nil, parent_sampled: nil, hub:, **options)
16
18
  super(**options)
17
19
 
18
20
  @name = name
19
21
  @parent_sampled = parent_sampled
20
22
  @transaction = self
23
+ @hub = hub
24
+ @configuration = hub.configuration
25
+ @logger = configuration.logger
21
26
  init_span_recorder
22
27
  end
23
28
 
24
- def self.from_sentry_trace(sentry_trace, configuration: Sentry.configuration, **options)
25
- return unless configuration.tracing_enabled?
29
+ def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
30
+ return unless hub.configuration.tracing_enabled?
26
31
  return unless sentry_trace
27
32
 
28
33
  match = SENTRY_TRACE_REGEXP.match(sentry_trace)
@@ -36,7 +41,7 @@ module Sentry
36
41
  sampled_flag != "0"
37
42
  end
38
43
 
39
- new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, **options)
44
+ new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
40
45
  end
41
46
 
42
47
  def to_hash
@@ -58,7 +63,7 @@ module Sentry
58
63
  copy
59
64
  end
60
65
 
61
- def set_initial_sample_decision(sampling_context:, configuration: Sentry.configuration)
66
+ def set_initial_sample_decision(sampling_context:)
62
67
  unless configuration.tracing_enabled?
63
68
  @sampled = false
64
69
  return
@@ -78,17 +83,16 @@ module Sentry
78
83
  end
79
84
 
80
85
  transaction_description = generate_transaction_description
81
- logger = configuration.logger
82
86
 
83
87
  unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
84
88
  @sampled = false
85
- logger.warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
89
+ log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
86
90
  return
87
91
  end
88
92
 
89
93
  if sample_rate == 0.0 || sample_rate == false
90
94
  @sampled = false
91
- logger.debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
95
+ log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
92
96
  return
93
97
  end
94
98
 
@@ -99,15 +103,26 @@ module Sentry
99
103
  end
100
104
 
101
105
  if @sampled
102
- logger.debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
106
+ log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
103
107
  else
104
- logger.debug(
108
+ log_debug(
105
109
  "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
106
110
  )
107
111
  end
108
112
  end
109
113
 
110
114
  def finish(hub: nil)
115
+ if hub
116
+ log_warn(
117
+ <<~MSG
118
+ Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
119
+ Please use `Hub#start_transaction` with the designated hub.
120
+ MSG
121
+ )
122
+ end
123
+
124
+ hub ||= @hub
125
+
111
126
  super() # Span#finish doesn't take arguments
112
127
 
113
128
  if @name.nil?
@@ -116,7 +131,6 @@ module Sentry
116
131
 
117
132
  return unless @sampled || @parent_sampled
118
133
 
119
- hub ||= Sentry.get_current_hub
120
134
  event = hub.current_client.event_from_transaction(self)
121
135
  hub.capture_event(event)
122
136
  end
@@ -6,12 +6,17 @@ module Sentry
6
6
  PROTOCOL_VERSION = '5'
7
7
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
8
8
 
9
+ include LoggingHelper
10
+
9
11
  attr_accessor :configuration
12
+ attr_reader :logger, :rate_limits
10
13
 
11
14
  def initialize(configuration)
12
15
  @configuration = configuration
16
+ @logger = configuration.logger
13
17
  @transport_configuration = configuration.transport
14
18
  @dsn = configuration.dsn
19
+ @rate_limits = {}
15
20
  end
16
21
 
17
22
  def send_data(data, options = {})
@@ -19,8 +24,18 @@ module Sentry
19
24
  end
20
25
 
21
26
  def send_event(event)
27
+ event_hash = event.to_hash
28
+ item_type = get_item_type(event_hash)
29
+
22
30
  unless configuration.sending_allowed?
23
- configuration.logger.debug(LOGGER_PROGNAME) { "Event not sent: #{configuration.error_messages}" }
31
+ log_debug("Envelope [#{item_type}] not sent: #{configuration.error_messages}")
32
+
33
+ return
34
+ end
35
+
36
+ if is_rate_limited?(item_type)
37
+ log_info("Envelope [#{item_type}] not sent: rate limiting")
38
+
24
39
  return
25
40
  end
26
41
 
@@ -33,6 +48,35 @@ module Sentry
33
48
  event
34
49
  end
35
50
 
51
+ def is_rate_limited?(item_type)
52
+ # check category-specific limit
53
+ category_delay =
54
+ case item_type
55
+ when "transaction"
56
+ @rate_limits["transaction"]
57
+ else
58
+ @rate_limits["error"]
59
+ end
60
+
61
+ # check universal limit if not category limit
62
+ universal_delay = @rate_limits[nil]
63
+
64
+ delay =
65
+ if category_delay && universal_delay
66
+ if category_delay > universal_delay
67
+ category_delay
68
+ else
69
+ universal_delay
70
+ end
71
+ elsif category_delay
72
+ category_delay
73
+ else
74
+ universal_delay
75
+ end
76
+
77
+ !!delay && delay > Time.now
78
+ end
79
+
36
80
  def generate_auth_header
37
81
  now = Sentry.utc_now.to_i
38
82
  fields = {
@@ -50,7 +94,7 @@ module Sentry
50
94
  event_hash = event.to_hash
51
95
 
52
96
  event_id = event_hash[:event_id] || event_hash["event_id"]
53
- item_type = event_hash[:type] || event_hash["type"] || "event"
97
+ item_type = get_item_type(event_hash)
54
98
 
55
99
  envelope = <<~ENVELOPE
56
100
  {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{Sentry.utc_now.iso8601}"}
@@ -58,10 +102,16 @@ module Sentry
58
102
  #{JSON.generate(event_hash)}
59
103
  ENVELOPE
60
104
 
61
- configuration.logger.info(LOGGER_PROGNAME) { "Sending envelope [#{item_type}] #{event_id} to Sentry" }
105
+ log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
62
106
 
63
107
  envelope
64
108
  end
109
+
110
+ private
111
+
112
+ def get_item_type(event_hash)
113
+ event_hash[:type] || event_hash["type"] || "event"
114
+ end
65
115
  end
66
116
  end
67
117
 
@@ -7,6 +7,10 @@ module Sentry
7
7
  GZIP_THRESHOLD = 1024 * 30
8
8
  CONTENT_TYPE = 'application/x-sentry-envelope'
9
9
 
10
+ DEFAULT_DELAY = 60
11
+ RETRY_AFTER_HEADER = "retry-after"
12
+ RATE_LIMIT_HEADER = "x-sentry-rate-limits"
13
+
10
14
  attr_reader :conn, :adapter
11
15
 
12
16
  def initialize(*args)
@@ -24,18 +28,26 @@ module Sentry
24
28
  encoding = GZIP_ENCODING
25
29
  end
26
30
 
27
- conn.post @endpoint do |req|
31
+ response = conn.post @endpoint do |req|
28
32
  req.headers['Content-Type'] = CONTENT_TYPE
29
33
  req.headers['Content-Encoding'] = encoding
30
34
  req.headers['X-Sentry-Auth'] = generate_auth_header
31
35
  req.body = data
32
36
  end
37
+
38
+ if has_rate_limited_header?(response.headers)
39
+ handle_rate_limited_response(response.headers)
40
+ end
33
41
  rescue Faraday::Error => e
34
42
  error_info = e.message
35
43
 
36
44
  if e.response
37
- error_info += "\nbody: #{e.response[:body]}"
38
- error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
45
+ if e.response[:status] == 429
46
+ handle_rate_limited_response(e.response[:headers])
47
+ else
48
+ error_info += "\nbody: #{e.response[:body]}"
49
+ error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
50
+ end
39
51
  end
40
52
 
41
53
  raise Sentry::ExternalError, error_info
@@ -43,6 +55,64 @@ module Sentry
43
55
 
44
56
  private
45
57
 
58
+ def has_rate_limited_header?(headers)
59
+ headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
60
+ end
61
+
62
+ def handle_rate_limited_response(headers)
63
+ rate_limits =
64
+ if rate_limits = headers[RATE_LIMIT_HEADER]
65
+ parse_rate_limit_header(rate_limits)
66
+ elsif retry_after = headers[RETRY_AFTER_HEADER]
67
+ # although Sentry doesn't send a date string back
68
+ # based on HTTP specification, this could be a date string (instead of an integer)
69
+ retry_after = retry_after.to_i
70
+ retry_after = DEFAULT_DELAY if retry_after == 0
71
+
72
+ { nil => Time.now + retry_after }
73
+ else
74
+ { nil => Time.now + DEFAULT_DELAY }
75
+ end
76
+
77
+ rate_limits.each do |category, limit|
78
+ if current_limit = @rate_limits[category]
79
+ if current_limit < limit
80
+ @rate_limits[category] = limit
81
+ end
82
+ else
83
+ @rate_limits[category] = limit
84
+ end
85
+ end
86
+ end
87
+
88
+ def parse_rate_limit_header(rate_limit_header)
89
+ time = Time.now
90
+
91
+ result = {}
92
+
93
+ limits = rate_limit_header.split(",")
94
+ limits.each do |limit|
95
+ next if limit.nil? || limit.empty?
96
+
97
+ begin
98
+ retry_after, categories = limit.strip.split(":").first(2)
99
+ retry_after = time + retry_after.to_i
100
+ categories = categories.split(";")
101
+
102
+ if categories.empty?
103
+ result[nil] = retry_after
104
+ else
105
+ categories.each do |category|
106
+ result[category] = retry_after
107
+ end
108
+ end
109
+ rescue StandardError
110
+ end
111
+ end
112
+
113
+ result
114
+ end
115
+
46
116
  def should_compress?(data)
47
117
  @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
48
118
  end
@@ -50,7 +120,7 @@ module Sentry
50
120
  def set_conn
51
121
  server = @dsn.server
52
122
 
53
- configuration.logger.debug(LOGGER_PROGNAME) { "Sentry HTTP Transport connecting to #{server}" }
123
+ log_debug("Sentry HTTP Transport connecting to #{server}")
54
124
 
55
125
  Faraday.new(server, :ssl => ssl_configuration, :proxy => @transport_configuration.proxy) do |builder|
56
126
  @transport_configuration.faraday_builder&.call(builder)
@@ -0,0 +1,24 @@
1
+ module Sentry
2
+ module LoggingHelper
3
+ def log_error(message, exception, debug: false)
4
+ message = "#{message}: #{exception.message}"
5
+ message += "\n#{exception.backtrace.join("\n")}" if debug
6
+
7
+ logger.error(LOGGER_PROGNAME) do
8
+ message
9
+ end
10
+ end
11
+
12
+ def log_info(message)
13
+ logger.info(LOGGER_PROGNAME) { message }
14
+ end
15
+
16
+ def log_debug(message)
17
+ logger.debug(LOGGER_PROGNAME) { message }
18
+ end
19
+
20
+ def log_warn(message)
21
+ logger.warn(LOGGER_PROGNAME) { message }
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module Sentry
2
- VERSION = "4.3.2"
2
+ VERSION = "4.4.0-beta.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 4.4.0.pre.beta.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-06 00:00:00.000000000 Z
11
+ date: 2021-04-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -83,6 +83,7 @@ files:
83
83
  - lib/sentry/interfaces/threads.rb
84
84
  - lib/sentry/linecache.rb
85
85
  - lib/sentry/logger.rb
86
+ - lib/sentry/net/http.rb
86
87
  - lib/sentry/rack.rb
87
88
  - lib/sentry/rack/capture_exceptions.rb
88
89
  - lib/sentry/rack/deprecations.rb
@@ -97,6 +98,7 @@ files:
97
98
  - lib/sentry/transport/http_transport.rb
98
99
  - lib/sentry/utils/argument_checking_helper.rb
99
100
  - lib/sentry/utils/exception_cause_chain.rb
101
+ - lib/sentry/utils/logging_helper.rb
100
102
  - lib/sentry/utils/real_ip.rb
101
103
  - lib/sentry/utils/request_id.rb
102
104
  - lib/sentry/version.rb
@@ -120,9 +122,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
122
  version: '2.4'
121
123
  required_rubygems_version: !ruby/object:Gem::Requirement
122
124
  requirements:
123
- - - ">="
125
+ - - ">"
124
126
  - !ruby/object:Gem::Version
125
- version: '0'
127
+ version: 1.3.1
126
128
  requirements: []
127
129
  rubygems_version: 3.0.3.1
128
130
  signing_key: