sentry-ruby 5.28.1 → 6.5.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +26 -2
  3. data/README.md +3 -3
  4. data/lib/sentry/background_worker.rb +1 -4
  5. data/lib/sentry/backtrace/line.rb +99 -0
  6. data/lib/sentry/backtrace.rb +44 -76
  7. data/lib/sentry/baggage.rb +2 -2
  8. data/lib/sentry/breadcrumb.rb +1 -1
  9. data/lib/sentry/breadcrumb_buffer.rb +2 -2
  10. data/lib/sentry/check_in_event.rb +2 -2
  11. data/lib/sentry/client.rb +57 -135
  12. data/lib/sentry/configuration.rb +155 -75
  13. data/lib/sentry/cron/monitor_check_ins.rb +3 -3
  14. data/lib/sentry/cron/monitor_config.rb +2 -2
  15. data/lib/sentry/cron/monitor_schedule.rb +2 -2
  16. data/lib/sentry/dsn.rb +33 -1
  17. data/lib/sentry/envelope/item.rb +3 -3
  18. data/lib/sentry/error_event.rb +3 -3
  19. data/lib/sentry/event.rb +4 -10
  20. data/lib/sentry/exceptions.rb +3 -0
  21. data/lib/sentry/hub.rb +26 -4
  22. data/lib/sentry/interface.rb +1 -1
  23. data/lib/sentry/interfaces/exception.rb +2 -2
  24. data/lib/sentry/interfaces/request.rb +2 -0
  25. data/lib/sentry/interfaces/single_exception.rb +4 -4
  26. data/lib/sentry/interfaces/stacktrace.rb +3 -3
  27. data/lib/sentry/interfaces/stacktrace_builder.rb +0 -8
  28. data/lib/sentry/interfaces/threads.rb +2 -2
  29. data/lib/sentry/log_event.rb +24 -142
  30. data/lib/sentry/log_event_buffer.rb +13 -60
  31. data/lib/sentry/metric_event.rb +49 -0
  32. data/lib/sentry/metric_event_buffer.rb +28 -0
  33. data/lib/sentry/metrics.rb +47 -54
  34. data/lib/sentry/profiler.rb +4 -5
  35. data/lib/sentry/propagation_context.rb +48 -8
  36. data/lib/sentry/rack/capture_exceptions.rb +90 -2
  37. data/lib/sentry/release_detector.rb +1 -1
  38. data/lib/sentry/rspec.rb +1 -1
  39. data/lib/sentry/scope.rb +51 -18
  40. data/lib/sentry/sequel.rb +35 -0
  41. data/lib/sentry/span.rb +5 -17
  42. data/lib/sentry/std_lib_logger.rb +4 -0
  43. data/lib/sentry/telemetry_event_buffer.rb +130 -0
  44. data/lib/sentry/test_helper.rb +8 -0
  45. data/lib/sentry/transaction.rb +53 -103
  46. data/lib/sentry/transaction_event.rb +4 -9
  47. data/lib/sentry/transport/http_transport.rb +7 -11
  48. data/lib/sentry/transport.rb +9 -7
  49. data/lib/sentry/utils/encoding_helper.rb +6 -0
  50. data/lib/sentry/utils/logging_helper.rb +25 -9
  51. data/lib/sentry/utils/telemetry_attributes.rb +30 -0
  52. data/lib/sentry/vernier/profiler.rb +4 -3
  53. data/lib/sentry/version.rb +1 -1
  54. data/lib/sentry-ruby.rb +53 -30
  55. data/sentry-ruby-core.gemspec +1 -1
  56. data/sentry-ruby.gemspec +2 -1
  57. metadata +27 -16
  58. data/lib/sentry/metrics/aggregator.rb +0 -248
  59. data/lib/sentry/metrics/configuration.rb +0 -57
  60. data/lib/sentry/metrics/counter_metric.rb +0 -25
  61. data/lib/sentry/metrics/distribution_metric.rb +0 -25
  62. data/lib/sentry/metrics/gauge_metric.rb +0 -35
  63. data/lib/sentry/metrics/local_aggregator.rb +0 -53
  64. data/lib/sentry/metrics/metric.rb +0 -19
  65. data/lib/sentry/metrics/set_metric.rb +0 -28
  66. data/lib/sentry/metrics/timing.rb +0 -51
@@ -7,9 +7,6 @@ require "sentry/propagation_context"
7
7
 
8
8
  module Sentry
9
9
  class Transaction < Span
10
- # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
11
- SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
12
-
13
10
  UNLABELD_NAME = "<unlabeled transaction>"
14
11
  MESSAGE_PREFIX = "[Tracing]"
15
12
 
@@ -40,12 +37,6 @@ module Sentry
40
37
  # @return [Hash]
41
38
  attr_reader :measurements
42
39
 
43
- # @deprecated Use Sentry.get_current_hub instead.
44
- attr_reader :hub
45
-
46
- # @deprecated Use Sentry.configuration instead.
47
- attr_reader :configuration
48
-
49
40
  # The effective sample rate at which this transaction was sampled.
50
41
  # @return [Float, nil]
51
42
  attr_reader :effective_sample_rate
@@ -63,7 +54,6 @@ module Sentry
63
54
  attr_reader :sample_rand
64
55
 
65
56
  def initialize(
66
- hub:,
67
57
  name: nil,
68
58
  source: :custom,
69
59
  parent_sampled: nil,
@@ -75,26 +65,14 @@ module Sentry
75
65
 
76
66
  set_name(name, source: source)
77
67
  @parent_sampled = parent_sampled
78
- @hub = hub
79
68
  @baggage = baggage
80
- @configuration = hub.configuration # to be removed
81
- @tracing_enabled = hub.configuration.tracing_enabled?
82
- @traces_sampler = hub.configuration.traces_sampler
83
- @traces_sample_rate = hub.configuration.traces_sample_rate
84
- @sdk_logger = hub.configuration.sdk_logger
85
- @release = hub.configuration.release
86
- @environment = hub.configuration.environment
87
- @dsn = hub.configuration.dsn
88
69
  @effective_sample_rate = nil
89
70
  @contexts = {}
90
71
  @measurements = {}
91
72
  @sample_rand = sample_rand
92
73
 
93
- unless @hub.profiler_running?
94
- @profiler = @configuration.profiler_class.new(@configuration)
95
- end
96
-
97
74
  init_span_recorder
75
+ init_profiler
98
76
 
99
77
  unless @sample_rand
100
78
  generator = Utils::SampleRand.new(trace_id: @trace_id)
@@ -102,65 +80,8 @@ module Sentry
102
80
  end
103
81
  end
104
82
 
105
- # @deprecated use Sentry.continue_trace instead.
106
- #
107
- # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
108
- #
109
- # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
110
- #
111
- # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
112
- # @param sentry_trace [String] the trace string from the previous transaction.
113
- # @param baggage [String, nil] the incoming baggage header string.
114
- # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
115
- # @param options [Hash] the options you want to use to initialize a Transaction instance.
116
- # @return [Transaction, nil]
117
- def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
118
- return unless hub.configuration.tracing_enabled?
119
- return unless sentry_trace
120
-
121
- sentry_trace_data = extract_sentry_trace(sentry_trace)
122
- return unless sentry_trace_data
123
-
124
- trace_id, parent_span_id, parent_sampled = sentry_trace_data
125
-
126
- baggage =
127
- if baggage && !baggage.empty?
128
- Baggage.from_incoming_header(baggage)
129
- else
130
- # If there's an incoming sentry-trace but no incoming baggage header,
131
- # for instance in traces coming from older SDKs,
132
- # baggage will be empty and frozen and won't be populated as head SDK.
133
- Baggage.new({})
134
- end
135
-
136
- baggage.freeze!
137
-
138
- sample_rand = extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
139
-
140
- new(
141
- trace_id: trace_id,
142
- parent_span_id: parent_span_id,
143
- parent_sampled: parent_sampled,
144
- hub: hub,
145
- baggage: baggage,
146
- sample_rand: sample_rand,
147
- **options
148
- )
149
- end
150
-
151
- # @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead.
152
- # @return [Array, nil]
153
- def self.extract_sentry_trace(sentry_trace)
154
- PropagationContext.extract_sentry_trace(sentry_trace)
155
- end
156
-
157
- def self.extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
158
- PropagationContext.extract_sample_rand_from_baggage(baggage, trace_id) ||
159
- PropagationContext.generate_sample_rand(baggage, trace_id, parent_sampled)
160
- end
161
-
162
83
  # @return [Hash]
163
- def to_hash
84
+ def to_h
164
85
  hash = super
165
86
 
166
87
  hash.merge!(
@@ -207,7 +128,9 @@ module Sentry
207
128
  # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
208
129
  # @return [void]
209
130
  def set_initial_sample_decision(sampling_context:)
210
- unless @tracing_enabled
131
+ configuration = Sentry.configuration
132
+
133
+ unless configuration && configuration.tracing_enabled?
211
134
  @sampled = false
212
135
  return
213
136
  end
@@ -218,12 +141,12 @@ module Sentry
218
141
  end
219
142
 
220
143
  sample_rate =
221
- if @traces_sampler.is_a?(Proc)
222
- @traces_sampler.call(sampling_context)
144
+ if configuration.traces_sampler.is_a?(Proc)
145
+ configuration.traces_sampler.call(sampling_context)
223
146
  elsif !sampling_context[:parent_sampled].nil?
224
147
  sampling_context[:parent_sampled]
225
148
  else
226
- @traces_sample_rate
149
+ configuration.traces_sample_rate
227
150
  end
228
151
 
229
152
  transaction_description = generate_transaction_description
@@ -265,29 +188,28 @@ module Sentry
265
188
  end
266
189
 
267
190
  # Finishes the transaction's recording and send it to Sentry.
268
- # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
269
191
  # @return [TransactionEvent]
270
- def finish(hub: nil, end_timestamp: nil)
271
- if hub
272
- log_warn(
273
- <<~MSG
274
- Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
275
- Please use `Hub#start_transaction` with the designated hub.
276
- MSG
277
- )
278
- end
279
-
280
- hub ||= @hub
281
-
192
+ def finish(end_timestamp: nil)
282
193
  super(end_timestamp: end_timestamp)
283
194
 
284
195
  if @name.nil?
285
196
  @name = UNLABELD_NAME
286
197
  end
287
198
 
288
- @hub.stop_profiler!(self)
199
+ hub = Sentry.get_current_hub
200
+ return unless hub
289
201
 
290
- if @sampled
202
+ hub.stop_profiler!(self)
203
+
204
+ if @sampled && ignore_status_code?
205
+ @sampled = false
206
+
207
+ status_code = get_http_status_code
208
+ log_debug("#{MESSAGE_PREFIX} Discarding #{generate_transaction_description} due to ignored HTTP status code: #{status_code}")
209
+
210
+ hub.current_client.transport.record_lost_event(:event_processor, "transaction")
211
+ hub.current_client.transport.record_lost_event(:event_processor, "span")
212
+ elsif @sampled
291
213
  event = hub.current_client.event_from_transaction(self)
292
214
  hub.capture_event(event)
293
215
  else
@@ -345,6 +267,15 @@ module Sentry
345
267
  @span_recorder.add(self)
346
268
  end
347
269
 
270
+ def init_profiler
271
+ hub = Sentry.get_current_hub
272
+ return unless hub
273
+
274
+ unless hub.profiler_running?
275
+ @profiler = hub.configuration.profiler_class.new(hub.configuration)
276
+ end
277
+ end
278
+
348
279
  private
349
280
 
350
281
  def generate_transaction_description
@@ -355,14 +286,17 @@ module Sentry
355
286
  end
356
287
 
357
288
  def populate_head_baggage
289
+ configuration = Sentry.configuration
290
+
358
291
  items = {
359
292
  "trace_id" => trace_id,
360
293
  "sample_rate" => effective_sample_rate&.to_s,
361
294
  "sample_rand" => Utils::SampleRand.format(@sample_rand),
362
295
  "sampled" => sampled&.to_s,
363
- "environment" => @environment,
364
- "release" => @release,
365
- "public_key" => @dsn&.public_key
296
+ "environment" => configuration&.environment,
297
+ "release" => configuration&.release,
298
+ "public_key" => configuration&.dsn&.public_key,
299
+ "org_id" => configuration&.effective_org_id
366
300
  }
367
301
 
368
302
  items["transaction"] = name unless source_low_quality?
@@ -371,6 +305,22 @@ module Sentry
371
305
  @baggage = Baggage.new(items, mutable: false)
372
306
  end
373
307
 
308
+ def ignore_status_code?
309
+ trace_ignore_status_codes = Sentry.configuration&.trace_ignore_status_codes
310
+ return false unless trace_ignore_status_codes
311
+
312
+ status_code = get_http_status_code
313
+ return false unless status_code
314
+
315
+ trace_ignore_status_codes.any? do |ignored|
316
+ ignored.is_a?(Range) ? ignored.include?(status_code) : status_code == ignored
317
+ end
318
+ end
319
+
320
+ def get_http_status_code
321
+ @data && @data[Span::DataConventions::HTTP_STATUS_CODE]
322
+ end
323
+
374
324
  class SpanRecorder
375
325
  attr_reader :max_length, :spans
376
326
 
@@ -17,9 +17,6 @@ module Sentry
17
17
  # @return [Hash, nil]
18
18
  attr_accessor :profile
19
19
 
20
- # @return [Hash, nil]
21
- attr_accessor :metrics_summary
22
-
23
20
  def initialize(transaction:, **options)
24
21
  super(**options)
25
22
 
@@ -32,10 +29,9 @@ module Sentry
32
29
  self.tags = transaction.tags
33
30
  self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
34
31
  self.measurements = transaction.measurements
35
- self.metrics_summary = transaction.metrics_summary
36
32
 
37
33
  finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
38
- self.spans = finished_spans.map(&:to_hash)
34
+ self.spans = finished_spans.map(&:to_h)
39
35
 
40
36
  populate_profile(transaction)
41
37
  end
@@ -48,12 +44,11 @@ module Sentry
48
44
  end
49
45
 
50
46
  # @return [Hash]
51
- def to_hash
47
+ def to_h
52
48
  data = super
53
- data[:spans] = @spans.map(&:to_hash) if @spans
49
+ data[:spans] = @spans.map(&:to_h) if @spans
54
50
  data[:start_timestamp] = @start_timestamp
55
51
  data[:measurements] = @measurements
56
- data[:_metrics_summary] = @metrics_summary if @metrics_summary
57
52
  data
58
53
  end
59
54
 
@@ -62,7 +57,7 @@ module Sentry
62
57
  EMPTY_PROFILE = {}.freeze
63
58
 
64
59
  def populate_profile(transaction)
65
- profile_hash = transaction.profiler&.to_hash || EMPTY_PROFILE
60
+ profile_hash = transaction.profiler&.to_h || EMPTY_PROFILE
66
61
 
67
62
  return if profile_hash.empty?
68
63
 
@@ -49,6 +49,12 @@ module Sentry
49
49
 
50
50
  if response.code.match?(/\A2\d{2}/)
51
51
  handle_rate_limited_response(response) if has_rate_limited_header?(response)
52
+ elsif response.code == "413"
53
+ error_message = "HTTP 413: Envelope dropped due to exceeded size limit"
54
+ error_message += " (body: #{response.body})" if response.body && !response.body.empty?
55
+ log_warn(error_message)
56
+
57
+ raise Sentry::SizeExceededError, error_message
52
58
  elsif response.code == "429"
53
59
  log_debug("the server responded with status 429")
54
60
  handle_rate_limited_response(response)
@@ -69,17 +75,7 @@ module Sentry
69
75
  end
70
76
 
71
77
  def generate_auth_header
72
- return nil unless @dsn
73
-
74
- now = Sentry.utc_now.to_i
75
- fields = {
76
- "sentry_version" => PROTOCOL_VERSION,
77
- "sentry_client" => USER_AGENT,
78
- "sentry_timestamp" => now,
79
- "sentry_key" => @dsn.public_key
80
- }
81
- fields["sentry_secret"] = @dsn.secret_key if @dsn.secret_key
82
- "Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
78
+ @dsn&.generate_auth_header(client: USER_AGENT)
83
79
  end
84
80
 
85
81
  def conn
@@ -5,7 +5,7 @@ require "sentry/envelope"
5
5
 
6
6
  module Sentry
7
7
  class Transport
8
- PROTOCOL_VERSION = "7"
8
+ PROTOCOL_VERSION = DSN::PROTOCOL_VERSION
9
9
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
10
10
  CLIENT_REPORT_INTERVAL = 30
11
11
 
@@ -19,7 +19,8 @@ module Sentry
19
19
  :before_send,
20
20
  :event_processor,
21
21
  :insufficient_data,
22
- :backpressure
22
+ :backpressure,
23
+ :send_error
23
24
  ]
24
25
 
25
26
  include LoggingHelper
@@ -61,6 +62,10 @@ module Sentry
61
62
  log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
62
63
  send_data(data)
63
64
  end
65
+ rescue Sentry::SizeExceededError
66
+ serialized_items&.each do |item|
67
+ record_lost_event(:send_error, item.data_category)
68
+ end
64
69
  end
65
70
 
66
71
  def serialize_envelope(envelope)
@@ -113,7 +118,7 @@ module Sentry
113
118
 
114
119
  def envelope_from_event(event)
115
120
  # Convert to hash
116
- event_payload = event.to_hash
121
+ event_payload = event.to_h
117
122
  event_id = event_payload[:event_id] || event_payload["event_id"]
118
123
  item_type = event_payload[:type] || event_payload["type"]
119
124
 
@@ -124,10 +129,7 @@ module Sentry
124
129
  sent_at: Sentry.utc_now.iso8601
125
130
  }
126
131
 
127
- if event.is_a?(Event) && event.dynamic_sampling_context
128
- envelope_headers[:trace] = event.dynamic_sampling_context
129
- end
130
-
132
+ envelope_headers[:trace] = event.dynamic_sampling_context if event.dynamic_sampling_context
131
133
  envelope = Envelope.new(envelope_headers)
132
134
 
133
135
  if event.is_a?(LogEvent)
@@ -3,6 +3,8 @@
3
3
  module Sentry
4
4
  module Utils
5
5
  module EncodingHelper
6
+ MALFORMED_STRING = "<malformed-string>"
7
+
6
8
  def self.encode_to_utf_8(value)
7
9
  if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
8
10
  value = value.dup.force_encoding(Encoding::UTF_8)
@@ -17,6 +19,10 @@ module Sentry
17
19
 
18
20
  value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
19
21
  end
22
+
23
+ def self.safe_utf_8_string(value)
24
+ valid_utf_8?(value) ? value : MALFORMED_STRING
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -3,27 +3,43 @@
3
3
  module Sentry
4
4
  # @private
5
5
  module LoggingHelper
6
- # @!visibility private
7
- attr_reader :sdk_logger
8
-
9
6
  # @!visibility private
10
7
  def log_error(message, exception, debug: false)
11
8
  message = "#{message}: #{exception.message}"
12
- message += "\n#{exception.backtrace.join("\n")}" if debug
9
+ message += "\n#{exception.backtrace.join("\n")}" if debug && exception.backtrace
13
10
 
14
- sdk_logger.error(LOGGER_PROGNAME) do
15
- message
16
- end
11
+ sdk_logger&.error(LOGGER_PROGNAME) { message }
12
+ rescue StandardError => e
13
+ log_to_stderr(e, message)
17
14
  end
18
15
 
19
16
  # @!visibility private
20
17
  def log_debug(message)
21
- sdk_logger.debug(LOGGER_PROGNAME) { message }
18
+ sdk_logger&.debug(LOGGER_PROGNAME) { message }
19
+ rescue StandardError => e
20
+ log_to_stderr(e, message)
22
21
  end
23
22
 
24
23
  # @!visibility private
25
24
  def log_warn(message)
26
- sdk_logger.warn(LOGGER_PROGNAME) { message }
25
+ sdk_logger&.warn(LOGGER_PROGNAME) { message }
26
+ rescue StandardError => e
27
+ log_to_stderr(e, message)
28
+ end
29
+
30
+ # @!visibility private
31
+ def sdk_logger
32
+ @sdk_logger ||= Sentry.sdk_logger
33
+ end
34
+
35
+ # @!visibility private
36
+ def log_to_stderr(error, message)
37
+ error_msg = "Sentry SDK logging failed (#{error.class}: #{error.message}): #{message}".scrub(%q(<?>))
38
+ error_msg += "\n#{error.backtrace.map { |line| line.scrub(%q(<?>)) }.join("\n")}" if error.backtrace
39
+
40
+ $stderr.puts(error_msg)
41
+ rescue StandardError
42
+ # swallow everything – logging must never crash the app
27
43
  end
28
44
  end
29
45
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Sentry
6
+ module Utils
7
+ module TelemetryAttributes
8
+ private
9
+
10
+ def attribute_hash(value)
11
+ case value
12
+ when String
13
+ { value: value, type: "string" }
14
+ when TrueClass, FalseClass
15
+ { value: value, type: "boolean" }
16
+ when Integer
17
+ { value: value, type: "integer" }
18
+ when Float
19
+ { value: value, type: "double" }
20
+ else
21
+ begin
22
+ { value: JSON.generate(value), type: "string" }
23
+ rescue
24
+ { value: value, type: "string" }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -20,6 +20,7 @@ module Sentry
20
20
 
21
21
  @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled?
22
22
  @profiles_sample_rate = configuration.profiles_sample_rate
23
+ @profiles_sample_interval = configuration.profiles_sample_interval
23
24
  @project_root = configuration.project_root
24
25
  @app_dirs_pattern = configuration.app_dirs_pattern
25
26
  @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
@@ -56,7 +57,7 @@ module Sentry
56
57
  return unless @sampled
57
58
  return if @started
58
59
 
59
- @started = ::Vernier.start_profile
60
+ @started = ::Vernier.start_profile(interval: @profiles_sample_interval)
60
61
 
61
62
  log("Started")
62
63
 
@@ -90,9 +91,9 @@ module Sentry
90
91
  Thread.current.object_id
91
92
  end
92
93
 
93
- def to_hash
94
+ def to_h
94
95
  unless @sampled
95
- record_lost_event(:sample_rate)
96
+ record_lost_event(:sample_rate) if @profiling_enabled
96
97
  return EMPTY_RESULT
97
98
  end
98
99
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.28.1"
4
+ VERSION = "6.5.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -26,8 +26,8 @@ require "sentry/threaded_periodic_worker"
26
26
  require "sentry/session_flusher"
27
27
  require "sentry/backpressure_monitor"
28
28
  require "sentry/cron/monitor_check_ins"
29
- require "sentry/metrics"
30
29
  require "sentry/vernier/profiler"
30
+ require "sentry/metrics"
31
31
 
32
32
  [
33
33
  "sentry/rake",
@@ -59,7 +59,6 @@ module Sentry
59
59
  logger
60
60
  session_flusher
61
61
  backpressure_monitor
62
- metrics_aggregator
63
62
  exception_locals_tp
64
63
  ].freeze
65
64
 
@@ -93,10 +92,6 @@ module Sentry
93
92
  # @return [BackpressureMonitor, nil]
94
93
  attr_reader :backpressure_monitor
95
94
 
96
- # @!attribute [r] metrics_aggregator
97
- # @return [Metrics::Aggregator, nil]
98
- attr_reader :metrics_aggregator
99
-
100
95
  ##### Patch Registration #####
101
96
 
102
97
  # @!visibility private
@@ -252,7 +247,6 @@ module Sentry
252
247
  @background_worker = Sentry::BackgroundWorker.new(config)
253
248
  @session_flusher = config.session_tracking? ? Sentry::SessionFlusher.new(config, client) : nil
254
249
  @backpressure_monitor = config.enable_backpressure_handling ? Sentry::BackpressureMonitor.new(config, client) : nil
255
- @metrics_aggregator = config.metrics.enabled ? Sentry::Metrics::Aggregator.new(config, client) : nil
256
250
  exception_locals_tp.enable if config.include_local_variables
257
251
  at_exit { close }
258
252
  end
@@ -273,12 +267,6 @@ module Sentry
273
267
  @backpressure_monitor = nil
274
268
  end
275
269
 
276
- if @metrics_aggregator
277
- @metrics_aggregator.flush(force: true)
278
- @metrics_aggregator.kill
279
- @metrics_aggregator = nil
280
- end
281
-
282
270
  if client = get_current_client
283
271
  client.flush
284
272
 
@@ -635,24 +623,27 @@ module Sentry
635
623
  #
636
624
  # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol
637
625
  #
638
- # @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled
626
+ # @return [StructuredLogger] The structured logger instance or nil if logs are disabled
639
627
  def logger
640
- @logger ||=
641
- if configuration.enable_logs
642
- # Initialize the public-facing Structured Logger if logs are enabled
643
- # Use configured structured logger class or default to StructuredLogger
644
- # @see https://develop.sentry.dev/sdk/telemetry/logs/
645
- configuration.structured_logging.logger_class.new(configuration)
646
- else
647
- warn <<~STR
648
- [sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on.
649
- Use Sentry.configuration.sdk_logger for SDK-specific logging needs."
650
-
651
- Caller: #{caller.first}
652
- STR
653
-
654
- configuration.sdk_logger
655
- end
628
+ @logger ||= configuration.structured_logging.logger_class.new(configuration)
629
+ end
630
+
631
+ # Returns the metrics API for capturing custom metrics.
632
+ #
633
+ # @example Enable metrics
634
+ # Sentry.init do |config|
635
+ # config.dsn = "YOUR_DSN"
636
+ # config.enable_metrics = true
637
+ # end
638
+ #
639
+ # @example Usage
640
+ # Sentry.metrics.count("button.click", 1, attributes: { button_id: "submit" })
641
+ # Sentry.metrics.distribution("response.time", 120.5, unit: "millisecond")
642
+ # Sentry.metrics.gauge("cpu.usage", 75.2, unit: "percent")
643
+ #
644
+ # @return [Metrics] The metrics API
645
+ def metrics
646
+ Metrics
656
647
  end
657
648
 
658
649
  ##### Helpers #####
@@ -675,6 +666,38 @@ module Sentry
675
666
  META
676
667
  end
677
668
 
669
+ # Registers a callback function that retrieves the current external propagation context.
670
+ # This is used by OpenTelemetry integration to provide trace_id and span_id from OTel context.
671
+ #
672
+ # @param callback [Proc, nil] A callable that returns [trace_id, span_id] or nil
673
+ # @return [void]
674
+ #
675
+ # @example
676
+ # Sentry.register_external_propagation_context do
677
+ # span_context = OpenTelemetry::Trace.current_span.context
678
+ # return nil unless span_context.valid?
679
+ # [span_context.hex_trace_id, span_context.hex_span_id]
680
+ # end
681
+ def register_external_propagation_context(&callback)
682
+ @external_propagation_context_callback = callback
683
+ end
684
+
685
+ # Returns the external propagation context (trace_id, span_id) if a callback is registered.
686
+ #
687
+ # @return [Array<String>, nil] A tuple of [trace_id, span_id] or nil if no context is available
688
+ def get_external_propagation_context
689
+ return nil unless @external_propagation_context_callback
690
+
691
+ @external_propagation_context_callback.call
692
+ rescue => e
693
+ sdk_logger&.debug(LOGGER_PROGNAME) { "Error getting external propagation context: #{e.message}" } if initialized?
694
+ nil
695
+ end
696
+
697
+ def clear_external_propagation_context
698
+ @external_propagation_context_callback = nil
699
+ end
700
+
678
701
  # @!visibility private
679
702
  def utc_now
680
703
  Time.now.utc
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
12
12
  spec.homepage = "https://github.com/getsentry/sentry-ruby"
13
13
 
14
14
  spec.platform = Gem::Platform::RUBY
15
- spec.required_ruby_version = '>= 2.4'
15
+ spec.required_ruby_version = '>= 2.7'
16
16
  spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
17
17
  spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n")
18
18
 
data/sentry-ruby.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.license = 'MIT'
12
12
 
13
13
  spec.platform = Gem::Platform::RUBY
14
- spec.required_ruby_version = '>= 2.4'
14
+ spec.required_ruby_version = '>= 2.7'
15
15
  spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
16
16
  spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n")
17
17
 
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
32
32
  spec.add_dependency "bigdecimal"
33
+ spec.add_dependency "logger"
33
34
  end