sentry-ruby 5.17.1 → 5.17.3

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: 7ca3eee69c858f7d40c18a030c56f4c51539e6f2795223f6a83450f94e8c2984
4
- data.tar.gz: 8289b57931ebf5430eb399ed320a91372fea15c70e1603d45a6f5b49cba9cc16
3
+ metadata.gz: 917e0ed267815d8daeeb5f731a14642be3b901a1a09a9341cc6ca58750094496
4
+ data.tar.gz: db84b44742d0e847b6fd66dac5a3338e63d4497723979b7c71fbbea46eb50480
5
5
  SHA512:
6
- metadata.gz: 5cffbfa857b8dc671c9883b62cff1142abd39f83e0ac630c3a2843bf212486ed0b58318e4375065b4c36826394255e83ef02dc4828ff87f641fe7aacb742166d
7
- data.tar.gz: f38446fd3df90e81687388c0eb26560f158e883e2040083fdd288e8e64a7ebbd796033357f0101dc9b71ba81f1f66e7297c42f3b593988aa7c5052fe57bce905
6
+ metadata.gz: 75e7822da3cf46b8faf9fd3a2528cd56ad8f3d8d38b7e21d178f257695409d07c2e7d643dbc48b09f818d6324c3f96d083d03b8ac3faa00e4fff0a8ea2dcdfc3
7
+ data.tar.gz: 1348bd6636f8283af3d85934ffe414606072758939cc0027c70730f8711f933ab72d9ee23d49cd921ca3562dcd4a034f93255fe5f3eb257a0e4288815be840e5
data/lib/sentry/client.rb CHANGED
@@ -49,16 +49,17 @@ module Sentry
49
49
  return unless configuration.sending_allowed?
50
50
 
51
51
  if event.is_a?(ErrorEvent) && !configuration.sample_allowed?
52
- transport.record_lost_event(:sample_rate, 'event')
52
+ transport.record_lost_event(:sample_rate, 'error')
53
53
  return
54
54
  end
55
55
 
56
56
  event_type = event.is_a?(Event) ? event.type : event["type"]
57
+ data_category = Envelope::Item.data_category(event_type)
57
58
  event = scope.apply_to_event(event, hint)
58
59
 
59
60
  if event.nil?
60
61
  log_debug("Discarded event because one of the event processors returned nil")
61
- transport.record_lost_event(:event_processor, event_type)
62
+ transport.record_lost_event(:event_processor, data_category)
62
63
  return
63
64
  end
64
65
 
@@ -66,7 +67,7 @@ module Sentry
66
67
  dispatch_async_event(async_block, event, hint)
67
68
  elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
68
69
  queued = dispatch_background_event(event, hint)
69
- transport.record_lost_event(:queue_overflow, event_type) unless queued
70
+ transport.record_lost_event(:queue_overflow, data_category) unless queued
70
71
  else
71
72
  send_event(event, hint)
72
73
  end
@@ -77,6 +78,20 @@ module Sentry
77
78
  nil
78
79
  end
79
80
 
81
+ # Capture an envelope directly.
82
+ # @param envelope [Envelope] the envelope to be captured.
83
+ # @return [void]
84
+ def capture_envelope(envelope)
85
+ Sentry.background_worker.perform { send_envelope(envelope) }
86
+ end
87
+
88
+ # Flush pending events to Sentry.
89
+ # @return [void]
90
+ def flush
91
+ transport.flush if configuration.sending_to_dsn_allowed?
92
+ spotlight_transport.flush if spotlight_transport
93
+ end
94
+
80
95
  # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting.
81
96
  # @param exception [Exception] the exception to be reported.
82
97
  # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
@@ -88,9 +103,10 @@ module Sentry
88
103
  return if !ignore_exclusions && !@configuration.exception_class_allowed?(exception)
89
104
 
90
105
  integration_meta = Sentry.integrations[hint[:integration]]
106
+ mechanism = hint.delete(:mechanism) { Mechanism.new }
91
107
 
92
108
  ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
93
- event.add_exception_interface(exception)
109
+ event.add_exception_interface(exception, mechanism: mechanism)
94
110
  event.add_threads_interface(crashed: true)
95
111
  event.level = :error
96
112
  end
@@ -151,13 +167,14 @@ module Sentry
151
167
  # @!macro send_event
152
168
  def send_event(event, hint = nil)
153
169
  event_type = event.is_a?(Event) ? event.type : event["type"]
170
+ data_category = Envelope::Item.data_category(event_type)
154
171
 
155
172
  if event_type != TransactionEvent::TYPE && configuration.before_send
156
173
  event = configuration.before_send.call(event, hint)
157
174
 
158
175
  if event.nil?
159
176
  log_debug("Discarded event because before_send returned nil")
160
- transport.record_lost_event(:before_send, 'event')
177
+ transport.record_lost_event(:before_send, data_category)
161
178
  return
162
179
  end
163
180
  end
@@ -167,7 +184,7 @@ module Sentry
167
184
 
168
185
  if event.nil?
169
186
  log_debug("Discarded event because before_send_transaction returned nil")
170
- transport.record_lost_event(:before_send, 'transaction')
187
+ transport.record_lost_event(:before_send, data_category)
171
188
  return
172
189
  end
173
190
  end
@@ -178,7 +195,23 @@ module Sentry
178
195
  event
179
196
  rescue => e
180
197
  log_error("Event sending failed", e, debug: configuration.debug)
181
- transport.record_lost_event(:network_error, event_type)
198
+ transport.record_lost_event(:network_error, data_category)
199
+ raise
200
+ end
201
+
202
+ # Send an envelope directly to Sentry.
203
+ # @param envelope [Envelope] the envelope to be sent.
204
+ # @return [void]
205
+ def send_envelope(envelope)
206
+ transport.send_envelope(envelope) if configuration.sending_to_dsn_allowed?
207
+ spotlight_transport.send_envelope(envelope) if spotlight_transport
208
+ rescue => e
209
+ log_error("Envelope sending failed", e, debug: configuration.debug)
210
+
211
+ envelope.items.map(&:data_category).each do |data_category|
212
+ transport.record_lost_event(:network_error, data_category)
213
+ end
214
+
182
215
  raise
183
216
  end
184
217
 
@@ -18,6 +18,23 @@ module Sentry
18
18
  @headers[:type] || 'event'
19
19
  end
20
20
 
21
+ # rate limits and client reports use the data_category rather than envelope item type
22
+ def self.data_category(type)
23
+ case type
24
+ when 'session', 'attachment', 'transaction', 'profile' then type
25
+ when 'sessions' then 'session'
26
+ when 'check_in' then 'monitor'
27
+ when 'statsd', 'metric_meta' then 'metric_bucket'
28
+ when 'event' then 'error'
29
+ when 'client_report' then 'internal'
30
+ else 'default'
31
+ end
32
+ end
33
+
34
+ def data_category
35
+ self.class.data_category(type)
36
+ end
37
+
21
38
  def to_s
22
39
  [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
23
40
  end
@@ -27,12 +27,12 @@ module Sentry
27
27
  end
28
28
 
29
29
  # @!visibility private
30
- def add_exception_interface(exception)
30
+ def add_exception_interface(exception, mechanism:)
31
31
  if exception.respond_to?(:sentry_context)
32
32
  @extra.merge!(exception.sentry_context)
33
33
  end
34
34
 
35
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder, mechanism: mechanism)
36
36
  end
37
37
  end
38
38
  end
@@ -14,6 +14,10 @@ module Sentry
14
14
  def capture_exception(exception, **options, &block)
15
15
  options[:hint] ||= {}
16
16
  options[:hint][:integration] = integration_name
17
+
18
+ # within an integration, we usually intercept uncaught exceptions so we set handled to false.
19
+ options[:hint][:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false)
20
+
17
21
  Sentry.capture_exception(exception, **options, &block)
18
22
  end
19
23
 
@@ -14,3 +14,4 @@ require "sentry/interfaces/request"
14
14
  require "sentry/interfaces/single_exception"
15
15
  require "sentry/interfaces/stacktrace"
16
16
  require "sentry/interfaces/threads"
17
+ require "sentry/interfaces/mechanism"
@@ -24,17 +24,18 @@ module Sentry
24
24
  # @param stacktrace_builder [StacktraceBuilder]
25
25
  # @see SingleExceptionInterface#build_with_stacktrace
26
26
  # @see SingleExceptionInterface#initialize
27
+ # @param mechanism [Mechanism]
27
28
  # @return [ExceptionInterface]
28
- def self.build(exception:, stacktrace_builder:)
29
+ def self.build(exception:, stacktrace_builder:, mechanism:)
29
30
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
30
31
  processed_backtrace_ids = Set.new
31
32
 
32
33
  exceptions = exceptions.map do |e|
33
34
  if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
34
35
  processed_backtrace_ids << e.backtrace.object_id
35
- SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
36
+ SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
36
37
  else
37
- SingleExceptionInterface.new(exception: exception)
38
+ SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
38
39
  end
39
40
  end
40
41
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Mechanism < Interface
5
+ # Generic identifier, mostly the source integration for this exception.
6
+ # @return [String]
7
+ attr_accessor :type
8
+
9
+ # A manually captured exception has handled set to true,
10
+ # false if coming from an integration where we intercept an uncaught exception.
11
+ # Defaults to true here and will be set to false explicitly in integrations.
12
+ # @return [Boolean]
13
+ attr_accessor :handled
14
+
15
+ def initialize(type: 'generic', handled: true)
16
+ @type = type
17
+ @handled = handled
18
+ end
19
+ end
20
+ end
@@ -11,10 +11,10 @@ module Sentry
11
11
  OMISSION_MARK = "...".freeze
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
- attr_reader :type, :module, :thread_id, :stacktrace
14
+ attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
15
15
  attr_accessor :value
16
16
 
17
- def initialize(exception:, stacktrace: nil)
17
+ def initialize(exception:, mechanism:, stacktrace: nil)
18
18
  @type = exception.class.to_s
19
19
  exception_message =
20
20
  if exception.respond_to?(:detailed_message)
@@ -29,17 +29,19 @@ module Sentry
29
29
  @module = exception.class.to_s.split('::')[0...-1].join('::')
30
30
  @thread_id = Thread.current.object_id
31
31
  @stacktrace = stacktrace
32
+ @mechanism = mechanism
32
33
  end
33
34
 
34
35
  def to_hash
35
36
  data = super
36
37
  data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
38
+ data[:mechanism] = data[:mechanism].to_hash
37
39
  data
38
40
  end
39
41
 
40
42
  # patch this method if you want to change an exception's stacktrace frames
41
43
  # also see `StacktraceBuilder.build`.
42
- def self.build_with_stacktrace(exception:, stacktrace_builder:)
44
+ def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:)
43
45
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
44
46
 
45
47
  if locals = exception.instance_variable_get(:@sentry_locals)
@@ -61,7 +63,7 @@ module Sentry
61
63
  stacktrace.frames.last.vars = locals
62
64
  end
63
65
 
64
- new(exception: exception, stacktrace: stacktrace)
66
+ new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
65
67
  end
66
68
  end
67
69
  end
@@ -12,8 +12,18 @@ module Sentry
12
12
  # when we record code locations
13
13
  DEFAULT_STACKLEVEL = 4
14
14
 
15
- KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\/.-]+/
16
- VALUE_SANITIZATION_REGEX = /[^[[:word:]][[:digit:]][[:space:]]_:\/@\.{}\[\]$-]+/
15
+ KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.]+/
16
+ UNIT_SANITIZATION_REGEX = /[^a-zA-Z0-9_]+/
17
+ TAG_KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.\/]+/
18
+
19
+ TAG_VALUE_SANITIZATION_MAP = {
20
+ "\n" => "\\n",
21
+ "\r" => "\\r",
22
+ "\t" => "\\t",
23
+ "\\" => "\\\\",
24
+ "|" => "\\u{7c}",
25
+ "," => "\\u{2c}"
26
+ }
17
27
 
18
28
  METRIC_TYPES = {
19
29
  c: CounterMetric,
@@ -23,7 +33,7 @@ module Sentry
23
33
  }
24
34
 
25
35
  # exposed only for testing
26
- attr_reader :thread, :buckets, :flush_shift, :code_locations
36
+ attr_reader :client, :thread, :buckets, :flush_shift, :code_locations
27
37
 
28
38
  def initialize(configuration, client)
29
39
  @client = client
@@ -107,9 +117,7 @@ module Sentry
107
117
  end
108
118
  end
109
119
 
110
- Sentry.background_worker.perform do
111
- @client.transport.send_envelope(envelope)
112
- end
120
+ @client.capture_envelope(envelope)
113
121
  end
114
122
 
115
123
  def kill
@@ -182,9 +190,9 @@ module Sentry
182
190
  timestamp_buckets.map do |metric_key, metric|
183
191
  type, key, unit, tags = metric_key
184
192
  values = metric.serialize.join(':')
185
- sanitized_tags = tags.map { |k, v| "#{sanitize_key(k)}:#{sanitize_value(v)}" }.join(',')
193
+ sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(',')
186
194
 
187
- "#{sanitize_key(key)}@#{unit}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}"
195
+ "#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}"
188
196
  end
189
197
  end.flatten.join("\n")
190
198
  end
@@ -192,7 +200,7 @@ module Sentry
192
200
  def serialize_locations(timestamp, locations)
193
201
  mapping = locations.map do |meta_key, location|
194
202
  type, key, unit = meta_key
195
- mri = "#{type}:#{sanitize_key(key)}@#{unit}"
203
+ mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}"
196
204
 
197
205
  # note this needs to be an array but it really doesn't serve a purpose right now
198
206
  [mri, [location.merge(type: 'location')]]
@@ -205,8 +213,16 @@ module Sentry
205
213
  key.gsub(KEY_SANITIZATION_REGEX, '_')
206
214
  end
207
215
 
208
- def sanitize_value(value)
209
- value.gsub(VALUE_SANITIZATION_REGEX, '')
216
+ def sanitize_unit(unit)
217
+ unit.gsub(UNIT_SANITIZATION_REGEX, '')
218
+ end
219
+
220
+ def sanitize_tag_key(key)
221
+ key.gsub(TAG_KEY_SANITIZATION_REGEX, '')
222
+ end
223
+
224
+ def sanitize_tag_value(value)
225
+ value.chars.map { |c| TAG_VALUE_SANITIZATION_MAP[c] || c }.join
210
226
  end
211
227
 
212
228
  def get_transaction_name
@@ -4,6 +4,7 @@ module Sentry
4
4
  module Rack
5
5
  class CaptureExceptions
6
6
  ERROR_EVENT_ID_KEY = "sentry.error_event_id"
7
+ MECHANISM_TYPE = "rack"
7
8
 
8
9
  def initialize(app)
9
10
  @app = app
@@ -56,7 +57,7 @@ module Sentry
56
57
  end
57
58
 
58
59
  def capture_exception(exception, env)
59
- Sentry.capture_exception(exception).tap do |event|
60
+ Sentry.capture_exception(exception, hint: { mechanism: mechanism }).tap do |event|
60
61
  env[ERROR_EVENT_ID_KEY] = event.event_id if event
61
62
  end
62
63
  end
@@ -74,6 +75,10 @@ module Sentry
74
75
  transaction.set_http_status(status_code)
75
76
  transaction.finish
76
77
  end
78
+
79
+ def mechanism
80
+ Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
81
+ end
77
82
  end
78
83
  end
79
84
  end
data/lib/sentry/rake.rb CHANGED
@@ -8,7 +8,9 @@ module Sentry
8
8
  module Application
9
9
  # @api private
10
10
  def display_error_message(ex)
11
- Sentry.capture_exception(ex) do |scope|
11
+ mechanism = Sentry::Mechanism.new(type: 'rake', handled: false)
12
+
13
+ Sentry.capture_exception(ex, hint: { mechanism: mechanism }) do |scope|
12
14
  task_name = top_level_tasks.join(' ')
13
15
  scope.set_transaction_name(task_name, source: :task)
14
16
  scope.set_tag("rake_task", task_name)
@@ -20,12 +20,8 @@ module Sentry
20
20
 
21
21
  def flush
22
22
  return if @pending_aggregates.empty?
23
- envelope = pending_envelope
24
-
25
- Sentry.background_worker.perform do
26
- @client.transport.send_envelope(envelope)
27
- end
28
23
 
24
+ @client.capture_envelope(pending_envelope)
29
25
  @pending_aggregates = {}
30
26
  end
31
27
 
@@ -88,18 +88,9 @@ module Sentry
88
88
  [data, serialized_items]
89
89
  end
90
90
 
91
- def is_rate_limited?(item_type)
91
+ def is_rate_limited?(data_category)
92
92
  # check category-specific limit
93
- category_delay =
94
- case item_type
95
- when "transaction"
96
- @rate_limits["transaction"]
97
- when "sessions"
98
- @rate_limits["session"]
99
- else
100
- @rate_limits["error"]
101
- end
102
-
93
+ category_delay = @rate_limits[data_category]
103
94
  # check universal limit if not category limit
104
95
  universal_delay = @rate_limits[nil]
105
96
 
@@ -160,11 +151,11 @@ module Sentry
160
151
  envelope
161
152
  end
162
153
 
163
- def record_lost_event(reason, item_type)
154
+ def record_lost_event(reason, data_category)
164
155
  return unless @send_client_reports
165
156
  return unless CLIENT_REPORT_REASONS.include?(reason)
166
157
 
167
- @discarded_events[[reason, item_type]] += 1
158
+ @discarded_events[[reason, data_category]] += 1
168
159
  end
169
160
 
170
161
  def flush
@@ -184,11 +175,7 @@ module Sentry
184
175
  return nil if @discarded_events.empty?
185
176
 
186
177
  discarded_events_hash = @discarded_events.map do |key, val|
187
- reason, type = key
188
-
189
- # 'event' has to be mapped to 'error'
190
- category = type == 'event' ? 'error' : type
191
-
178
+ reason, category = key
192
179
  { reason: reason, category: category, quantity: val }
193
180
  end
194
181
 
@@ -206,9 +193,9 @@ module Sentry
206
193
 
207
194
  def reject_rate_limited_items(envelope)
208
195
  envelope.items.reject! do |item|
209
- if is_rate_limited?(item.type)
196
+ if is_rate_limited?(item.data_category)
210
197
  log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
211
- record_lost_event(:ratelimit_backoff, item.type)
198
+ record_lost_event(:ratelimit_backoff, item.data_category)
212
199
 
213
200
  true
214
201
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.17.1"
4
+ VERSION = "5.17.3"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -257,7 +257,7 @@ module Sentry
257
257
  end
258
258
 
259
259
  if client = get_current_client
260
- client.transport.flush
260
+ client.flush
261
261
 
262
262
  if client.configuration.include_local_variables
263
263
  exception_locals_tp.disable
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.17.1
4
+ version: 5.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-15 00:00:00.000000000 Z
11
+ date: 2024-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -89,6 +89,7 @@ files:
89
89
  - lib/sentry/integrable.rb
90
90
  - lib/sentry/interface.rb
91
91
  - lib/sentry/interfaces/exception.rb
92
+ - lib/sentry/interfaces/mechanism.rb
92
93
  - lib/sentry/interfaces/request.rb
93
94
  - lib/sentry/interfaces/single_exception.rb
94
95
  - lib/sentry/interfaces/stacktrace.rb