sentry-ruby-core 4.3.2 → 4.5.0.pre.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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