sentry-ruby-core 4.3.2 → 4.4.0.pre.beta.0

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: 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: