sentry-ruby 5.10.0 → 5.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +12 -13
  4. data/README.md +26 -11
  5. data/Rakefile +9 -11
  6. data/bin/console +2 -0
  7. data/lib/sentry/attachment.rb +40 -0
  8. data/lib/sentry/background_worker.rb +11 -5
  9. data/lib/sentry/backpressure_monitor.rb +45 -0
  10. data/lib/sentry/backtrace.rb +12 -9
  11. data/lib/sentry/baggage.rb +7 -7
  12. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  13. data/lib/sentry/breadcrumb.rb +13 -6
  14. data/lib/sentry/check_in_event.rb +61 -0
  15. data/lib/sentry/client.rb +214 -25
  16. data/lib/sentry/configuration.rb +221 -38
  17. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  18. data/lib/sentry/cron/configuration.rb +23 -0
  19. data/lib/sentry/cron/monitor_check_ins.rb +77 -0
  20. data/lib/sentry/cron/monitor_config.rb +53 -0
  21. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  22. data/lib/sentry/dsn.rb +4 -4
  23. data/lib/sentry/envelope/item.rb +88 -0
  24. data/lib/sentry/envelope.rb +2 -68
  25. data/lib/sentry/error_event.rb +2 -2
  26. data/lib/sentry/event.rb +28 -47
  27. data/lib/sentry/excon/middleware.rb +77 -0
  28. data/lib/sentry/excon.rb +10 -0
  29. data/lib/sentry/faraday.rb +77 -0
  30. data/lib/sentry/graphql.rb +9 -0
  31. data/lib/sentry/hub.rb +138 -6
  32. data/lib/sentry/integrable.rb +10 -0
  33. data/lib/sentry/interface.rb +1 -0
  34. data/lib/sentry/interfaces/exception.rb +5 -3
  35. data/lib/sentry/interfaces/mechanism.rb +20 -0
  36. data/lib/sentry/interfaces/request.rb +8 -8
  37. data/lib/sentry/interfaces/single_exception.rb +13 -9
  38. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  39. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  40. data/lib/sentry/linecache.rb +3 -3
  41. data/lib/sentry/log_event.rb +206 -0
  42. data/lib/sentry/log_event_buffer.rb +75 -0
  43. data/lib/sentry/logger.rb +1 -1
  44. data/lib/sentry/metrics/aggregator.rb +248 -0
  45. data/lib/sentry/metrics/configuration.rb +47 -0
  46. data/lib/sentry/metrics/counter_metric.rb +25 -0
  47. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  48. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  49. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  50. data/lib/sentry/metrics/metric.rb +19 -0
  51. data/lib/sentry/metrics/set_metric.rb +28 -0
  52. data/lib/sentry/metrics/timing.rb +51 -0
  53. data/lib/sentry/metrics.rb +56 -0
  54. data/lib/sentry/net/http.rb +27 -44
  55. data/lib/sentry/profiler/helpers.rb +46 -0
  56. data/lib/sentry/profiler.rb +41 -60
  57. data/lib/sentry/propagation_context.rb +135 -0
  58. data/lib/sentry/puma.rb +12 -5
  59. data/lib/sentry/rack/capture_exceptions.rb +17 -8
  60. data/lib/sentry/rack.rb +2 -2
  61. data/lib/sentry/rake.rb +4 -15
  62. data/lib/sentry/redis.rb +10 -4
  63. data/lib/sentry/release_detector.rb +5 -5
  64. data/lib/sentry/rspec.rb +91 -0
  65. data/lib/sentry/scope.rb +75 -39
  66. data/lib/sentry/session.rb +2 -2
  67. data/lib/sentry/session_flusher.rb +15 -43
  68. data/lib/sentry/span.rb +92 -8
  69. data/lib/sentry/std_lib_logger.rb +50 -0
  70. data/lib/sentry/structured_logger.rb +138 -0
  71. data/lib/sentry/test_helper.rb +42 -13
  72. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  73. data/lib/sentry/transaction.rb +44 -43
  74. data/lib/sentry/transaction_event.rb +10 -6
  75. data/lib/sentry/transport/configuration.rb +73 -1
  76. data/lib/sentry/transport/http_transport.rb +71 -41
  77. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  78. data/lib/sentry/transport.rb +53 -49
  79. data/lib/sentry/utils/argument_checking_helper.rb +12 -0
  80. data/lib/sentry/utils/env_helper.rb +21 -0
  81. data/lib/sentry/utils/http_tracing.rb +74 -0
  82. data/lib/sentry/utils/logging_helper.rb +10 -7
  83. data/lib/sentry/utils/real_ip.rb +2 -2
  84. data/lib/sentry/utils/request_id.rb +1 -1
  85. data/lib/sentry/utils/uuid.rb +13 -0
  86. data/lib/sentry/vernier/output.rb +89 -0
  87. data/lib/sentry/vernier/profiler.rb +132 -0
  88. data/lib/sentry/version.rb +1 -1
  89. data/lib/sentry-ruby.rb +206 -35
  90. data/sentry-ruby-core.gemspec +3 -1
  91. data/sentry-ruby.gemspec +15 -6
  92. metadata +61 -11
data/lib/sentry/client.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sentry/transport"
4
+ require "sentry/log_event"
5
+ require "sentry/log_event_buffer"
6
+ require "sentry/utils/uuid"
4
7
 
5
8
  module Sentry
6
9
  class Client
@@ -10,28 +13,38 @@ module Sentry
10
13
  # @return [Transport]
11
14
  attr_reader :transport
12
15
 
16
+ # The Transport object that'll send events for the client.
17
+ # @return [SpotlightTransport, nil]
18
+ attr_reader :spotlight_transport
19
+
20
+ # @!visibility private
21
+ attr_reader :log_event_buffer
22
+
13
23
  # @!macro configuration
14
24
  attr_reader :configuration
15
25
 
16
- # @deprecated Use Sentry.logger to retrieve the current logger instead.
17
- attr_reader :logger
18
-
19
26
  # @param configuration [Configuration]
20
27
  def initialize(configuration)
21
28
  @configuration = configuration
22
- @logger = configuration.logger
29
+ @sdk_logger = configuration.sdk_logger
23
30
 
24
31
  if transport_class = configuration.transport.transport_class
25
32
  @transport = transport_class.new(configuration)
26
33
  else
27
34
  @transport =
28
35
  case configuration.dsn&.scheme
29
- when 'http', 'https'
36
+ when "http", "https"
30
37
  HTTPTransport.new(configuration)
31
38
  else
32
39
  DummyTransport.new(configuration)
33
40
  end
34
41
  end
42
+
43
+ @spotlight_transport = SpotlightTransport.new(configuration) if configuration.spotlight
44
+
45
+ if configuration.enable_logs
46
+ @log_event_buffer = LogEventBuffer.new(configuration, self).start
47
+ end
35
48
  end
36
49
 
37
50
  # Applies the given scope's data to the event and sends it to Sentry.
@@ -42,25 +55,36 @@ module Sentry
42
55
  def capture_event(event, scope, hint = {})
43
56
  return unless configuration.sending_allowed?
44
57
 
45
- unless event.is_a?(TransactionEvent) || configuration.sample_allowed?
46
- transport.record_lost_event(:sample_rate, 'event')
58
+ if event.is_a?(ErrorEvent) && !configuration.sample_allowed?
59
+ transport.record_lost_event(:sample_rate, "error")
47
60
  return
48
61
  end
49
62
 
50
63
  event_type = event.is_a?(Event) ? event.type : event["type"]
64
+ data_category = Envelope::Item.data_category(event_type)
65
+
66
+ is_transaction = event.is_a?(TransactionEvent)
67
+ spans_before = is_transaction ? event.spans.size : 0
68
+
51
69
  event = scope.apply_to_event(event, hint)
52
70
 
53
71
  if event.nil?
54
- log_info("Discarded event because one of the event processors returned nil")
55
- transport.record_lost_event(:event_processor, event_type)
72
+ log_debug("Discarded event because one of the event processors returned nil")
73
+ transport.record_lost_event(:event_processor, data_category)
74
+ transport.record_lost_event(:event_processor, "span", num: spans_before + 1) if is_transaction
56
75
  return
76
+ elsif is_transaction
77
+ spans_delta = spans_before - event.spans.size
78
+ transport.record_lost_event(:event_processor, "span", num: spans_delta) if spans_delta > 0
57
79
  end
58
80
 
59
81
  if async_block = configuration.async
60
82
  dispatch_async_event(async_block, event, hint)
61
83
  elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
62
- queued = dispatch_background_event(event, hint)
63
- transport.record_lost_event(:queue_overflow, event_type) unless queued
84
+ unless dispatch_background_event(event, hint)
85
+ transport.record_lost_event(:queue_overflow, data_category)
86
+ transport.record_lost_event(:queue_overflow, "span", num: spans_before + 1) if is_transaction
87
+ end
64
88
  else
65
89
  send_event(event, hint)
66
90
  end
@@ -71,6 +95,30 @@ module Sentry
71
95
  nil
72
96
  end
73
97
 
98
+ # Buffer a log event to be sent later with other logs in a single envelope
99
+ # @param event [LogEvent] the log event to be buffered
100
+ # @return [LogEvent]
101
+ def buffer_log_event(event, scope)
102
+ return unless event.is_a?(LogEvent)
103
+ @log_event_buffer.add_event(scope.apply_to_event(event))
104
+ event
105
+ end
106
+
107
+ # Capture an envelope directly.
108
+ # @param envelope [Envelope] the envelope to be captured.
109
+ # @return [void]
110
+ def capture_envelope(envelope)
111
+ Sentry.background_worker.perform { send_envelope(envelope) }
112
+ end
113
+
114
+ # Flush pending events to Sentry.
115
+ # @return [void]
116
+ def flush
117
+ transport.flush if configuration.sending_to_dsn_allowed?
118
+ spotlight_transport.flush if spotlight_transport
119
+ @log_event_buffer&.flush
120
+ end
121
+
74
122
  # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting.
75
123
  # @param exception [Exception] the exception to be reported.
76
124
  # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
@@ -82,9 +130,10 @@ module Sentry
82
130
  return if !ignore_exclusions && !@configuration.exception_class_allowed?(exception)
83
131
 
84
132
  integration_meta = Sentry.integrations[hint[:integration]]
133
+ mechanism = hint.delete(:mechanism) { Mechanism.new }
85
134
 
86
135
  ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
87
- event.add_exception_interface(exception)
136
+ event.add_exception_interface(exception, mechanism: mechanism)
88
137
  event.add_threads_interface(crashed: true)
89
138
  event.level = :error
90
139
  end
@@ -104,6 +153,53 @@ module Sentry
104
153
  event
105
154
  end
106
155
 
156
+ # Initializes a CheckInEvent object with the given options.
157
+ #
158
+ # @param slug [String] identifier of this monitor
159
+ # @param status [Symbol] status of this check-in, one of {CheckInEvent::VALID_STATUSES}
160
+ # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
161
+ # @param duration [Integer, nil] seconds elapsed since this monitor started
162
+ # @param monitor_config [Cron::MonitorConfig, nil] configuration for this monitor
163
+ # @param check_in_id [String, nil] for updating the status of an existing monitor
164
+ #
165
+ # @return [Event]
166
+ def event_from_check_in(
167
+ slug,
168
+ status,
169
+ hint = {},
170
+ duration: nil,
171
+ monitor_config: nil,
172
+ check_in_id: nil
173
+ )
174
+ return unless configuration.sending_allowed?
175
+
176
+ CheckInEvent.new(
177
+ configuration: configuration,
178
+ integration_meta: Sentry.integrations[hint[:integration]],
179
+ slug: slug,
180
+ status: status,
181
+ duration: duration,
182
+ monitor_config: monitor_config,
183
+ check_in_id: check_in_id
184
+ )
185
+ end
186
+
187
+ # Initializes a LogEvent object with the given message and options
188
+ #
189
+ # @param message [String] the log message
190
+ # @param level [Symbol] the log level (:trace, :debug, :info, :warn, :error, :fatal)
191
+ # @param options [Hash] additional options
192
+ # @option options [Array] :parameters Array of values to replace template tokens in the message
193
+ #
194
+ # @return [LogEvent] the created log event
195
+ def event_from_log(message, level:, **options)
196
+ return unless configuration.sending_allowed?
197
+
198
+ attributes = options.reject { |k, _| k == :level || k == :severity }
199
+
200
+ LogEvent.new(level: level, body: message, attributes: attributes)
201
+ end
202
+
107
203
  # Initializes an Event object with the given Transaction object.
108
204
  # @param transaction [Transaction] the transaction to be recorded.
109
205
  # @return [TransactionEvent]
@@ -114,13 +210,26 @@ module Sentry
114
210
  # @!macro send_event
115
211
  def send_event(event, hint = nil)
116
212
  event_type = event.is_a?(Event) ? event.type : event["type"]
213
+ data_category = Envelope::Item.data_category(event_type)
214
+ spans_before = event.is_a?(TransactionEvent) ? event.spans.size : 0
117
215
 
118
216
  if event_type != TransactionEvent::TYPE && configuration.before_send
119
217
  event = configuration.before_send.call(event, hint)
120
218
 
121
- if event.nil?
122
- log_info("Discarded event because before_send returned nil")
123
- transport.record_lost_event(:before_send, 'event')
219
+ case event
220
+ when ErrorEvent, CheckInEvent
221
+ # do nothing
222
+ when Hash
223
+ log_debug(<<~MSG)
224
+ Returning a Hash from before_send is deprecated and will be removed in the next major version.
225
+ Please return a Sentry::ErrorEvent object instead.
226
+ MSG
227
+ else
228
+ # Avoid serializing the event object in this case because we aren't sure what it is and what it contains
229
+ log_debug(<<~MSG)
230
+ Discarded event because before_send didn't return a Sentry::ErrorEvent object but an instance of #{event.class}
231
+ MSG
232
+ transport.record_lost_event(:before_send, data_category)
124
233
  return
125
234
  end
126
235
  end
@@ -128,26 +237,104 @@ module Sentry
128
237
  if event_type == TransactionEvent::TYPE && configuration.before_send_transaction
129
238
  event = configuration.before_send_transaction.call(event, hint)
130
239
 
131
- if event.nil?
132
- log_info("Discarded event because before_send_transaction returned nil")
133
- transport.record_lost_event(:before_send, 'transaction')
240
+ if event.is_a?(TransactionEvent) || event.is_a?(Hash)
241
+ spans_after = event.is_a?(TransactionEvent) ? event.spans.size : 0
242
+ spans_delta = spans_before - spans_after
243
+ transport.record_lost_event(:before_send, "span", num: spans_delta) if spans_delta > 0
244
+
245
+ if event.is_a?(Hash)
246
+ log_debug(<<~MSG)
247
+ Returning a Hash from before_send_transaction is deprecated and will be removed in the next major version.
248
+ Please return a Sentry::TransactionEvent object instead.
249
+ MSG
250
+ end
251
+ else
252
+ # Avoid serializing the event object in this case because we aren't sure what it is and what it contains
253
+ log_debug(<<~MSG)
254
+ Discarded event because before_send_transaction didn't return a Sentry::TransactionEvent object but an instance of #{event.class}
255
+ MSG
256
+ transport.record_lost_event(:before_send, "transaction")
257
+ transport.record_lost_event(:before_send, "span", num: spans_before + 1)
134
258
  return
135
259
  end
136
260
  end
137
261
 
138
- transport.send_event(event)
262
+ transport.send_event(event) if configuration.sending_to_dsn_allowed?
263
+ spotlight_transport.send_event(event) if spotlight_transport
139
264
 
140
265
  event
141
266
  rescue => e
142
- loggable_event_type = event_type.capitalize
143
- log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
267
+ log_error("Event sending failed", e, debug: configuration.debug)
268
+ transport.record_lost_event(:network_error, data_category)
269
+ transport.record_lost_event(:network_error, "span", num: spans_before + 1) if event.is_a?(TransactionEvent)
270
+ raise
271
+ end
272
+
273
+ # Send an envelope with batched logs
274
+ # @param log_events [Array<LogEvent>] the log events to be sent
275
+ # @api private
276
+ # @return [void]
277
+ def send_logs(log_events)
278
+ envelope = Envelope.new(
279
+ event_id: Sentry::Utils.uuid,
280
+ sent_at: Sentry.utc_now.iso8601,
281
+ dsn: configuration.dsn,
282
+ sdk: Sentry.sdk_meta
283
+ )
284
+
285
+ discarded_count = 0
286
+ envelope_items = []
287
+
288
+ if configuration.before_send_log
289
+ log_events.each do |log_event|
290
+ processed_log_event = configuration.before_send_log.call(log_event)
291
+
292
+ if processed_log_event
293
+ envelope_items << processed_log_event.to_hash
294
+ else
295
+ discarded_count += 1
296
+ end
297
+ end
298
+
299
+ envelope_items
300
+ else
301
+ envelope_items = log_events.map(&:to_hash)
302
+ end
303
+
304
+ envelope.add_item(
305
+ {
306
+ type: "log",
307
+ item_count: envelope_items.size,
308
+ content_type: "application/vnd.sentry.items.log+json"
309
+ },
310
+ { items: envelope_items }
311
+ )
312
+
313
+ send_envelope(envelope)
314
+
315
+ unless discarded_count.zero?
316
+ transport.record_lost_event(:before_send, "log_item", num: discarded_count)
317
+ end
318
+ end
319
+
320
+ # Send an envelope directly to Sentry.
321
+ # @param envelope [Envelope] the envelope to be sent.
322
+ # @return [void]
323
+ def send_envelope(envelope)
324
+ transport.send_envelope(envelope) if configuration.sending_to_dsn_allowed?
325
+ spotlight_transport.send_envelope(envelope) if spotlight_transport
326
+ rescue => e
327
+ log_error("Envelope sending failed", e, debug: configuration.debug)
328
+
329
+ envelope.items.map(&:data_category).each do |data_category|
330
+ transport.record_lost_event(:network_error, data_category)
331
+ end
144
332
 
145
- event_info = Event.get_log_message(event.to_hash)
146
- log_info("Unreported #{loggable_event_type}: #{event_info}")
147
- transport.record_lost_event(:network_error, event_type)
148
333
  raise
149
334
  end
150
335
 
336
+ # @deprecated use Sentry.get_traceparent instead.
337
+ #
151
338
  # Generates a Sentry trace for distribted tracing from the given Span.
152
339
  # Returns `nil` if `config.propagate_traces` is `false`.
153
340
  # @param span [Span] the span to generate trace from.
@@ -160,7 +347,9 @@ module Sentry
160
347
  trace
161
348
  end
162
349
 
163
- # Generates a W3C Baggage header for distribted tracing from the given Span.
350
+ # @deprecated Use Sentry.get_baggage instead.
351
+ #
352
+ # Generates a W3C Baggage header for distributed tracing from the given Span.
164
353
  # Returns `nil` if `config.propagate_traces` is `false`.
165
354
  # @param span [Span] the span to generate trace from.
166
355
  # @return [String, nil]