sentry-ruby-core 4.3.2 → 4.5.0.pre.beta.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: 85c558cf77534e3d3fea7d30e417171302f485042b8a09b3a1114c714f1b17ec
4
- data.tar.gz: d8077c3cfee56f1427d03770dc8d28e9920bbba4d97e14e983d3b7a5a949be95
3
+ metadata.gz: 39b0d720876099298375f0ea0130ced0effa7fe7cb3c153d5075cff400788d7a
4
+ data.tar.gz: 3e89ef0214a1b632274605984514aa4703d6aea3bcc6c625d8f32eaa2f552203
5
5
  SHA512:
6
- metadata.gz: 9cc8fa4030f678d6b2cd34d1c5752e4da6065527ed598e6f43a14ab6adcb44fbe4f104b99de9dd65d45995f726fc074bf3944891348ce4491a385ce22acfa493
7
- data.tar.gz: 8fc20069fbddcb6dba3206ea8d7578c9da54f5a0812c0388e9e1afc983befcaaaa3d32390af78b6f3bee6294e997455dbcc807d7543380bc20740b314534dd7b
6
+ metadata.gz: 263a733c7d5c2ca356a1de8f5b75b7ec1266e1ea6abe57b20db0a4789a2bd7bb1444dababc06a1291089c009dbcae05b8ded8a11bcd21f721ece3df454e1fb8b
7
+ data.tar.gz: b256b35de1a11df8e0b3468d47365fec0646da828f05e3161cd4b0374a5b7fda4586d911146895d04bb6574306688b587012c6b85d3b09baf096771efb3d1e49
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,6 @@ 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
data/CHANGELOG.md CHANGED
@@ -1,5 +1,97 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Features
6
+
7
+ - Implement sentry-trace propagation [#1446](https://github.com/getsentry/sentry-ruby/pull/1446)
8
+
9
+ The SDK will insert the `sentry-trace` to outgoing requests made with `Net::HTTP`. Its value would look like `d827317d25d5420aa3aa97a0257db998-57757614642bdff5-1`.
10
+
11
+ If the receiver service also uses Sentry and the SDK supports performance monitoring, its tracing event will be connected with the sender application's.
12
+
13
+ Example:
14
+
15
+ <img width="1283" alt="connect sentry trace" src="https://user-images.githubusercontent.com/5079556/118963250-d7b40980-b998-11eb-9de4-598d1b220137.png">
16
+
17
+ This feature is activated by default. But users can use the new `config.propagate_traces` config option to disable it.
18
+
19
+ ### Bug Fixes
20
+
21
+ - Allow toggling background sending on the fly [#1447](https://github.com/getsentry/sentry-ruby/pull/1447)
22
+
23
+ ## 4.4.2
24
+
25
+ - Fix NoMethodError when SDK's dsn is nil [#1433](https://github.com/getsentry/sentry-ruby/pull/1433)
26
+ - fix: Update protocol version to 7 [#1434](https://github.com/getsentry/sentry-ruby/pull/1434)
27
+ - Fixes [#867](https://github.com/getsentry/sentry-ruby/issues/867)
28
+
29
+ ## 4.4.1
30
+
31
+ - Apply patches when initializing the SDK [#1432](https://github.com/getsentry/sentry-ruby/pull/1432)
32
+
33
+ ## 4.4.0
34
+
35
+ ### Features
36
+
37
+ #### Support category-based rate limiting [#1336](https://github.com/getsentry/sentry-ruby/pull/1336)
38
+
39
+ 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:
40
+
41
+ ```
42
+ Unable to record event with remote Sentry server (Sentry::Error - the server responded with status 429
43
+ body: {"detail":"event rejected due to rate limit"}):
44
+ ```
45
+
46
+ This change improves the SDK's handling on such responses by:
47
+
48
+ - Not treating them as errors, so you don't see the noise anymore.
49
+ - Halting event sending for a while according to the duration provided in the response. And warns you with a message like:
50
+
51
+ ```
52
+ Envelope [event] not sent: Excluded by random sample
53
+ ```
54
+
55
+ #### Record request span from Net::HTTP library [#1381](https://github.com/getsentry/sentry-ruby/pull/1381)
56
+
57
+ Now any outgoing requests will be recorded as a tracing span. Example:
58
+
59
+ <img width="60%" alt="net:http span example" src="https://user-images.githubusercontent.com/5079556/115838944-c1279a80-a44c-11eb-8c67-dfd92bf68bbd.png">
60
+
61
+
62
+ #### Record breadcrumb for Net::HTTP requests [#1394](https://github.com/getsentry/sentry-ruby/pull/1394)
63
+
64
+ With the new `http_logger` breadcrumbs logger:
65
+
66
+ ```ruby
67
+ config.breadcrumbs_logger = [:http_logger]
68
+ ```
69
+
70
+ The SDK now records a new `net.http` breadcrumb whenever the user makes a request with the `Net::HTTP` library.
71
+
72
+ <img width="60%" alt="net http breadcrumb" src="https://user-images.githubusercontent.com/5079556/114298326-5f7c3d80-9ae8-11eb-9108-222384a7f1a2.png">
73
+
74
+ #### Support config.debug configuration option [#1400](https://github.com/getsentry/sentry-ruby/pull/1400)
75
+
76
+ 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.
77
+
78
+ #### Add the third tracing state [#1402](https://github.com/getsentry/sentry-ruby/pull/1402)
79
+ - `rate == 0` - Tracing enabled. Rejects all locally created transactions but respects sentry-trace.
80
+ - `1 > rate > 0` - Tracing enabled. Samples locally created transactions with the rate and respects sentry-trace.
81
+ - `rate < 0` or `rate > 1` - Tracing disabled.
82
+
83
+ ### Refactorings
84
+
85
+ - Let Transaction constructor take an optional hub argument [#1384](https://github.com/getsentry/sentry-ruby/pull/1384)
86
+ - Introduce LoggingHelper [#1385](https://github.com/getsentry/sentry-ruby/pull/1385)
87
+ - Raise exception if a Transaction is initialized without a hub [#1391](https://github.com/getsentry/sentry-ruby/pull/1391)
88
+ - Make hub a required argument for Transaction constructor [#1401](https://github.com/getsentry/sentry-ruby/pull/1401)
89
+
90
+ ### Bug Fixes
91
+
92
+ - Check `Scope#set_context`'s value argument [#1415](https://github.com/getsentry/sentry-ruby/pull/1415)
93
+ - Disable tracing if events are not allowed to be sent [#1421](https://github.com/getsentry/sentry-ruby/pull/1421)
94
+
3
95
  ## 4.3.2
4
96
 
5
97
  - 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,7 @@ 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"
9
10
  require "sentry/configuration"
10
11
  require "sentry/logger"
11
12
  require "sentry/event"
@@ -31,6 +32,8 @@ module Sentry
31
32
 
32
33
  LOGGER_PROGNAME = "sentry".freeze
33
34
 
35
+ SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze
36
+
34
37
  THREAD_LOCAL = :sentry_hub
35
38
 
36
39
  def self.sdk_meta
@@ -62,9 +65,26 @@ module Sentry
62
65
 
63
66
  attr_accessor :background_worker
64
67
 
68
+ @@registered_patches = []
69
+
70
+ def register_patch(&block)
71
+ registered_patches << block
72
+ end
73
+
74
+ def apply_patches(config)
75
+ registered_patches.each do |patch|
76
+ patch.call(config)
77
+ end
78
+ end
79
+
80
+ def registered_patches
81
+ @@registered_patches
82
+ end
83
+
65
84
  def init(&block)
66
85
  config = Configuration.new
67
86
  yield(config) if block_given?
87
+ apply_patches(config)
68
88
  client = Client.new(config)
69
89
  scope = Scope.new(max_breadcrumbs: config.max_breadcrumbs)
70
90
  hub = Hub.new(client, scope)
@@ -188,3 +208,6 @@ module Sentry
188
208
  end
189
209
  end
190
210
  end
211
+
212
+ # patches
213
+ require "sentry/net/http"
@@ -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)
@@ -28,7 +30,7 @@ module Sentry
28
30
 
29
31
  if async_block = configuration.async
30
32
  dispatch_async_event(async_block, event, hint)
31
- elsif hint.fetch(:background, true)
33
+ elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
32
34
  dispatch_background_event(event, hint)
33
35
  else
34
36
  send_event(event, hint)
@@ -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,11 +88,21 @@ 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
 
98
+ def generate_sentry_trace(span)
99
+ return unless configuration.propagate_traces
100
+
101
+ trace = span.to_sentry_trace
102
+ log_debug("[Tracing] Adding #{SENTRY_TRACE_HEADER_NAME} header to outgoing request: #{trace}")
103
+ trace
104
+ end
105
+
94
106
  private
95
107
 
96
108
  def dispatch_background_event(event, hint)
@@ -112,9 +124,8 @@ module Sentry
112
124
  end
113
125
  rescue => e
114
126
  loggable_event_type = event_hash["type"] || "event"
115
- logger.error(LOGGER_PROGNAME) { "Async #{loggable_event_type} sending failed: #{e.message}" }
127
+ log_error("Async #{loggable_event_type} sending failed", e, debug: configuration.debug)
116
128
  send_event(event, hint)
117
129
  end
118
-
119
130
  end
120
131
  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
 
@@ -101,6 +106,9 @@ module Sentry
101
106
  # Set automatically for Rails.
102
107
  attr_reader :project_root
103
108
 
109
+ # Insert sentry-trace to outgoing requests' headers
110
+ attr_accessor :propagate_traces
111
+
104
112
  # Array of rack env parameters to be included in the event sent to sentry.
105
113
  attr_accessor :rack_env_whitelist
106
114
 
@@ -167,13 +175,12 @@ module Sentry
167
175
  LOG_PREFIX = "** [Sentry] ".freeze
168
176
  MODULE_SEPARATOR = "::".freeze
169
177
 
170
- AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
171
-
172
178
  # Post initialization callbacks are called at the end of initialization process
173
179
  # allowing extending the configuration of sentry-ruby by multiple extensions
174
180
  @@post_initialization_callbacks = []
175
181
 
176
182
  def initialize
183
+ self.debug = false
177
184
  self.background_worker_threads = Concurrent.processor_count
178
185
  self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
179
186
  self.breadcrumbs_logger = []
@@ -186,6 +193,7 @@ module Sentry
186
193
  self.linecache = ::Sentry::LineCache.new
187
194
  self.logger = ::Sentry::Logger.new(STDOUT)
188
195
  self.project_root = Dir.pwd
196
+ self.propagate_traces = true
189
197
 
190
198
  self.release = detect_release
191
199
  self.sample_rate = 1.0
@@ -226,10 +234,6 @@ module Sentry
226
234
  if logger.is_a?(Array)
227
235
  logger
228
236
  else
229
- unless AVAILABLE_BREADCRUMBS_LOGGERS.include?(logger)
230
- raise Sentry::Error, "Unsupported breadcrumbs logger. Supported loggers: #{AVAILABLE_BREADCRUMBS_LOGGERS}"
231
- end
232
-
233
237
  Array(logger)
234
238
  end
235
239
 
@@ -278,10 +282,10 @@ module Sentry
278
282
  def exception_class_allowed?(exc)
279
283
  if exc.is_a?(Sentry::Error)
280
284
  # Try to prevent error reporting loops
281
- logger.debug(LOGGER_PROGNAME) { "Refusing to capture Sentry error: #{exc.inspect}" }
285
+ log_debug("Refusing to capture Sentry error: #{exc.inspect}")
282
286
  false
283
287
  elsif excluded_exception?(exc)
284
- logger.debug(LOGGER_PROGNAME) { "User excluded error: #{exc.inspect}" }
288
+ log_debug("User excluded error: #{exc.inspect}")
285
289
  false
286
290
  else
287
291
  true
@@ -293,7 +297,7 @@ module Sentry
293
297
  end
294
298
 
295
299
  def tracing_enabled?
296
- !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
300
+ !!((@traces_sample_rate && @traces_sample_rate >= 0.0 && @traces_sample_rate <= 1.0) || @traces_sampler) && sending_allowed?
297
301
  end
298
302
 
299
303
  def stacktrace_builder
@@ -314,7 +318,7 @@ module Sentry
314
318
  detect_release_from_capistrano ||
315
319
  detect_release_from_heroku
316
320
  rescue => e
317
- logger.error(LOGGER_PROGNAME) { "Error detecting release: #{e.message}" }
321
+ log_error("Error detecting release", e, debug: debug)
318
322
  end
319
323
 
320
324
  def excluded_exception?(incoming_exception)
@@ -349,7 +353,7 @@ module Sentry
349
353
  def detect_release_from_heroku
350
354
  return unless running_on_heroku?
351
355
  return if ENV['CI']
352
- logger.warn(LOGGER_PROGNAME) { HEROKU_DYNO_METADATA_MESSAGE } && return unless ENV['HEROKU_SLUG_COMMIT']
356
+ log_warn(HEROKU_DYNO_METADATA_MESSAGE) && return unless ENV['HEROKU_SLUG_COMMIT']
353
357
 
354
358
  ENV['HEROKU_SLUG_COMMIT']
355
359
  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,126 @@
1
+ require "net/http"
2
+
3
+ module Sentry
4
+ module Net
5
+ module HTTP
6
+ OP_NAME = "net.http"
7
+
8
+ # To explain how the entire thing works, we need to know how the original Net::HTTP#request works
9
+ # Here's part of its definition. As you can see, it usually calls itself inside a #start block
10
+ #
11
+ # ```
12
+ # def request(req, body = nil, &block)
13
+ # unless started?
14
+ # start {
15
+ # req['connection'] ||= 'close'
16
+ # return request(req, body, &block) # <- request will be called for the second time from the first call
17
+ # }
18
+ # end
19
+ # # .....
20
+ # end
21
+ # ```
22
+ #
23
+ # So when the entire flow looks like this:
24
+ #
25
+ # 1. #request is called.
26
+ # - But because the request hasn't started yet, it calls #start (which then calls #do_start)
27
+ # - At this moment @sentry_span is still nil, so #set_sentry_trace_header returns early
28
+ # 2. #do_start then creates a new Span and assigns it to @sentry_span
29
+ # 3. #request is called for the second time.
30
+ # - This time @sentry_span should present. So #set_sentry_trace_header will set the sentry-trace header on the request object
31
+ # 4. Once the request finished, it
32
+ # - Records a breadcrumb if http_logger is set
33
+ # - Finishes the Span inside @sentry_span and clears the instance variable
34
+ #
35
+ def request(req, body = nil, &block)
36
+ set_sentry_trace_header(req)
37
+
38
+ super.tap do |res|
39
+ record_sentry_breadcrumb(req, res)
40
+ record_sentry_span(req, res)
41
+ end
42
+ end
43
+
44
+ def do_start
45
+ super.tap do
46
+ start_sentry_span
47
+ end
48
+ end
49
+
50
+ def do_finish
51
+ super.tap do
52
+ finish_sentry_span
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def set_sentry_trace_header(req)
59
+ return unless @sentry_span
60
+
61
+ trace = Sentry.get_current_client.generate_sentry_trace(@sentry_span)
62
+ req[SENTRY_TRACE_HEADER_NAME] = trace if trace
63
+ end
64
+
65
+ def record_sentry_breadcrumb(req, res)
66
+ if Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
67
+ return if from_sentry_sdk?
68
+
69
+ request_info = extract_request_info(req)
70
+ crumb = Sentry::Breadcrumb.new(
71
+ level: :info,
72
+ category: OP_NAME,
73
+ type: :info,
74
+ data: {
75
+ method: request_info[:method],
76
+ url: request_info[:url],
77
+ status: res.code.to_i
78
+ }
79
+ )
80
+ Sentry.add_breadcrumb(crumb)
81
+ end
82
+ end
83
+
84
+ def record_sentry_span(req, res)
85
+ if Sentry.initialized? && @sentry_span
86
+ request_info = extract_request_info(req)
87
+ @sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
88
+ @sentry_span.set_data(:status, res.code.to_i)
89
+ end
90
+ end
91
+
92
+ def start_sentry_span
93
+ if Sentry.initialized? && transaction = Sentry.get_current_scope.get_transaction
94
+ return if from_sentry_sdk?
95
+ return if transaction.sampled == false
96
+
97
+ child_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
98
+ @sentry_span = child_span
99
+ end
100
+ end
101
+
102
+ def finish_sentry_span
103
+ if Sentry.initialized? && @sentry_span
104
+ @sentry_span.set_timestamp(Sentry.utc_now.to_f)
105
+ @sentry_span = nil
106
+ end
107
+ end
108
+
109
+ def from_sentry_sdk?
110
+ dsn = Sentry.configuration.dsn
111
+ dsn && dsn.host == self.address
112
+ end
113
+
114
+ def extract_request_info(req)
115
+ uri = req.uri
116
+ url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
117
+ { method: req.method, url: url }
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ Sentry.register_patch do
124
+ patch = Sentry::Net::HTTP
125
+ Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
126
+ end
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
@@ -3,15 +3,20 @@ require "base64"
3
3
 
4
4
  module Sentry
5
5
  class Transport
6
- PROTOCOL_VERSION = '5'
6
+ PROTOCOL_VERSION = '7'
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.5.0-beta.1"
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.5.0.pre.beta.1
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-05-27 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,11 +122,11 @@ 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
- rubygems_version: 3.0.3.1
129
+ rubygems_version: 3.1.6
128
130
  signing_key:
129
131
  specification_version: 4
130
132
  summary: A gem that provides a client interface for the Sentry error logger