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
@@ -10,8 +10,6 @@ module Sentry
10
10
 
11
11
  VERSION = "1"
12
12
  PLATFORM = "ruby"
13
- # 101 Hz in microseconds
14
- DEFAULT_INTERVAL = 1e6 / 101
15
13
  MICRO_TO_NANO_SECONDS = 1e3
16
14
  MIN_SAMPLES_REQUIRED = 2
17
15
 
@@ -24,6 +22,7 @@ module Sentry
24
22
 
25
23
  @profiling_enabled = defined?(StackProf) && configuration.profiling_enabled?
26
24
  @profiles_sample_rate = configuration.profiles_sample_rate
25
+ @profiles_sample_interval = configuration.profiles_sample_interval
27
26
  @project_root = configuration.project_root
28
27
  @app_dirs_pattern = configuration.app_dirs_pattern
29
28
  @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
@@ -32,7 +31,7 @@ module Sentry
32
31
  def start
33
32
  return unless @sampled
34
33
 
35
- @started = StackProf.start(interval: DEFAULT_INTERVAL,
34
+ @started = StackProf.start(interval: @profiles_sample_interval,
36
35
  mode: :wall,
37
36
  raw: true,
38
37
  aggregate: false)
@@ -81,9 +80,9 @@ module Sentry
81
80
  log("Discarding profile due to sampling decision") unless @sampled
82
81
  end
83
82
 
84
- def to_hash
83
+ def to_h
85
84
  unless @sampled
86
- record_lost_event(:sample_rate)
85
+ record_lost_event(:sample_rate) if @profiling_enabled
87
86
  return {}
88
87
  end
89
88
 
@@ -53,6 +53,44 @@ module Sentry
53
53
  [trace_id, parent_span_id, parent_sampled]
54
54
  end
55
55
 
56
+ # Determines whether we should continue an incoming trace based on org_id matching
57
+ # and the strict_trace_continuation configuration option.
58
+ #
59
+ # @param incoming_baggage [Baggage] the baggage from the incoming request
60
+ # @return [Boolean]
61
+ def self.should_continue_trace?(incoming_baggage)
62
+ return true unless Sentry.initialized?
63
+
64
+ configuration = Sentry.configuration
65
+ sdk_org_id = configuration.effective_org_id
66
+ baggage_org_id = incoming_baggage.items["org_id"]
67
+
68
+ # Mismatched org IDs always start a new trace regardless of strict mode
69
+ if sdk_org_id && baggage_org_id && sdk_org_id != baggage_org_id
70
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
71
+ "Starting a new trace because org IDs don't match (incoming baggage org_id: #{baggage_org_id}, SDK org_id: #{sdk_org_id})"
72
+ end
73
+
74
+ return false
75
+ end
76
+
77
+ return true unless configuration.strict_trace_continuation
78
+
79
+ # In strict mode, both must be present and match (unless both are missing)
80
+ if sdk_org_id.nil? && baggage_org_id.nil?
81
+ true
82
+ elsif sdk_org_id.nil? || baggage_org_id.nil?
83
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
84
+ "Starting a new trace because strict trace continuation is enabled and one org ID is missing " \
85
+ "(incoming baggage org_id: #{baggage_org_id.inspect}, SDK org_id: #{sdk_org_id.inspect})"
86
+ end
87
+
88
+ false
89
+ else
90
+ true
91
+ end
92
+ end
93
+
56
94
  def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
57
95
  return unless baggage&.items
58
96
 
@@ -96,9 +134,7 @@ module Sentry
96
134
  sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
97
135
 
98
136
  if sentry_trace_data
99
- @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
100
-
101
- @baggage =
137
+ incoming_baggage =
102
138
  if baggage_header && !baggage_header.empty?
103
139
  Baggage.from_incoming_header(baggage_header)
104
140
  else
@@ -108,10 +144,13 @@ module Sentry
108
144
  Baggage.new({})
109
145
  end
110
146
 
111
- @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
112
-
113
- @baggage.freeze!
114
- @incoming_trace = true
147
+ if self.class.should_continue_trace?(incoming_baggage)
148
+ @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
149
+ @baggage = incoming_baggage
150
+ @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
151
+ @baggage.freeze!
152
+ @incoming_trace = true
153
+ end
115
154
  end
116
155
  end
117
156
  end
@@ -162,7 +201,8 @@ module Sentry
162
201
  "sample_rand" => Utils::SampleRand.format(@sample_rand),
163
202
  "environment" => configuration.environment,
164
203
  "release" => configuration.release,
165
- "public_key" => configuration.dsn&.public_key
204
+ "public_key" => configuration.dsn&.public_key,
205
+ "org_id" => configuration.effective_org_id
166
206
  }
167
207
 
168
208
  items.compact!
@@ -33,7 +33,7 @@ module Sentry
33
33
  raise # Don't capture Sentry errors
34
34
  rescue Exception => e
35
35
  capture_exception(e, env)
36
- finish_transaction(transaction, 500)
36
+ finish_transaction(transaction, status_code_for_exception(e))
37
37
  raise
38
38
  end
39
39
 
@@ -72,7 +72,16 @@ module Sentry
72
72
  }
73
73
 
74
74
  transaction = Sentry.continue_trace(env, **options)
75
- Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
75
+ transaction = Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
76
+ attach_queue_time(transaction, env)
77
+ transaction
78
+ end
79
+
80
+ def attach_queue_time(transaction, env)
81
+ return unless transaction
82
+ return unless (queue_time = extract_queue_time(env))
83
+
84
+ transaction.set_data(Span::DataConventions::HTTP_QUEUE_TIME_MS, queue_time)
76
85
  end
77
86
 
78
87
 
@@ -86,6 +95,85 @@ module Sentry
86
95
  def mechanism
87
96
  Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
88
97
  end
98
+
99
+ # Extracts queue time from the request environment.
100
+ # Calculates the time (in milliseconds) the request spent waiting in the
101
+ # web server queue before processing began.
102
+ #
103
+ # Subtracts puma.request_body_wait to account for time spent waiting for
104
+ # slow clients to send the request body, isolating actual queue time.
105
+ # See: https://github.com/puma/puma/blob/master/docs/architecture.md
106
+ #
107
+ # @param env [Hash] Rack env
108
+ # @return [Float, nil] queue time in milliseconds or nil
109
+ def extract_queue_time(env)
110
+ return unless Sentry.configuration&.capture_queue_time
111
+
112
+ header_value = env["HTTP_X_REQUEST_START"]
113
+ return unless header_value
114
+
115
+ request_start = parse_request_start_header(header_value)
116
+ return unless request_start
117
+
118
+ total_time_ms = ((Time.now.to_f - request_start) * 1000).round(2)
119
+
120
+ # reject negative (clock skew between proxy & app server)
121
+ return unless total_time_ms >= 0
122
+
123
+ puma_wait_ms = env["puma.request_body_wait"]
124
+ puma_wait_ms = puma_wait_ms.to_f if puma_wait_ms.is_a?(String)
125
+
126
+ if puma_wait_ms && puma_wait_ms > 0
127
+ queue_time_ms = total_time_ms - puma_wait_ms
128
+ queue_time_ms >= 0 ? queue_time_ms : 0.0 # more sanity check
129
+ else
130
+ total_time_ms
131
+ end
132
+ rescue StandardError
133
+ end
134
+
135
+ # Parses X-Request-Start header value to extract a timestamp.
136
+ # Supports multiple formats:
137
+ # - Nginx: "t=1234567890.123" (seconds with decimal)
138
+ # - Heroku, HAProxy 1.9+: "t=1234567890123456" (microseconds)
139
+ # - HAProxy < 1.9: "t=1234567890" (seconds)
140
+ # - Generic: "1234567890.123" (raw timestamp)
141
+ #
142
+ # @param header_value [String] The X-Request-Start header value
143
+ # @return [Float, nil] Timestamp in seconds since epoch or nil
144
+ def parse_request_start_header(header_value)
145
+ return unless header_value
146
+
147
+ # Take the first value if comma-separated (multiple headers collapsed by a proxy)
148
+ # and strip surrounding whitespace from each token
149
+ raw = header_value.split(",").first.to_s.strip
150
+
151
+ timestamp = if raw.start_with?("t=")
152
+ value = raw[2..-1].strip
153
+ return nil unless value.match?(/\A\d+(?:\.\d+)?\z/)
154
+ value.to_f
155
+ elsif raw.match?(/\A\d+(?:\.\d+)?\z/)
156
+ raw.to_f
157
+ else
158
+ return
159
+ end
160
+
161
+ # normalize: timestamps can be in seconds, milliseconds or microseconds
162
+ # any timestamp > 10 trillion = microseconds
163
+ if timestamp > 10_000_000_000_000
164
+ timestamp / 1_000_000.0
165
+ # timestamp > 10 billion & < 10 trillion = milliseconds
166
+ elsif timestamp > 10_000_000_000
167
+ timestamp / 1_000.0
168
+ else
169
+ timestamp # assume seconds
170
+ end
171
+ rescue StandardError
172
+ end
173
+
174
+ def status_code_for_exception(exception)
175
+ 500
176
+ end
89
177
  end
90
178
  end
91
179
  end
@@ -13,7 +13,7 @@ module Sentry
13
13
 
14
14
  def detect_release_from_heroku(running_on_heroku)
15
15
  return unless running_on_heroku
16
- ENV["HEROKU_SLUG_COMMIT"]
16
+ ENV["HEROKU_BUILD_COMMIT"] || ENV["HEROKU_SLUG_COMMIT"]
17
17
  end
18
18
 
19
19
  def detect_release_from_capistrano(project_root)
data/lib/sentry/rspec.rb CHANGED
@@ -70,7 +70,7 @@ RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts|
70
70
  end
71
71
 
72
72
  def dump_events(sentry_events)
73
- sentry_events.map(&Kernel.method(:Hash)).map do |hash|
73
+ sentry_events.map(&:to_h).map do |hash|
74
74
  hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) }
75
75
  end.map do |hash|
76
76
  JSON.pretty_generate(hash)
data/lib/sentry/scope.rb CHANGED
@@ -46,7 +46,7 @@ module Sentry
46
46
  # @param hint [Hash] the hint data that'll be passed to event processors.
47
47
  # @return [Event]
48
48
  def apply_to_event(event, hint = nil)
49
- unless event.is_a?(CheckInEvent) || event.is_a?(LogEvent)
49
+ unless event.is_a?(CheckInEvent)
50
50
  event.tags = tags.merge(event.tags)
51
51
  event.user = user.merge(event.user)
52
52
  event.extra = extra.merge(event.extra)
@@ -60,23 +60,10 @@ module Sentry
60
60
  event.attachments = attachments
61
61
  end
62
62
 
63
- if event.is_a?(LogEvent)
64
- event.user = user.merge(event.user)
65
- end
66
-
67
- if span
68
- event.contexts[:trace] ||= span.get_trace_context
69
-
70
- if event.respond_to?(:dynamic_sampling_context)
71
- event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
72
- end
73
- else
74
- event.contexts[:trace] ||= propagation_context.get_trace_context
75
-
76
- if event.respond_to?(:dynamic_sampling_context)
77
- event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
78
- end
79
- end
63
+ trace_context = get_trace_context
64
+ dynamic_sampling_context = trace_context.delete(:dynamic_sampling_context)
65
+ event.contexts[:trace] ||= trace_context
66
+ event.dynamic_sampling_context ||= dynamic_sampling_context
80
67
 
81
68
  all_event_processors = self.class.global_event_processors + @event_processors
82
69
 
@@ -89,6 +76,37 @@ module Sentry
89
76
  event
90
77
  end
91
78
 
79
+ # A leaner version of apply_to_event that applies to
80
+ # lightweight payloads like Logs and Metrics.
81
+ #
82
+ # Adds trace_id, span_id, user from the scope and default attributes from configuration.
83
+ #
84
+ # @param telemetry [MetricEvent, LogEvent] the telemetry event to apply scope context to
85
+ # @return [MetricEvent, LogEvent] the telemetry event with scope context applied
86
+ def apply_to_telemetry(telemetry)
87
+ # TODO-neel when new scope set_attribute api is added: add them here
88
+ trace_context = get_trace_context
89
+ telemetry.trace_id = trace_context[:trace_id]
90
+ telemetry.span_id = trace_context[:span_id]
91
+
92
+ configuration = Sentry.configuration
93
+ return telemetry unless configuration
94
+
95
+ telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"]
96
+ telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"]
97
+ telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment
98
+ telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
99
+ telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
100
+
101
+ unless user.empty?
102
+ telemetry.attributes["user.id"] ||= user[:id] if user[:id]
103
+ telemetry.attributes["user.name"] ||= user[:username] if user[:username]
104
+ telemetry.attributes["user.email"] ||= user[:email] if user[:email]
105
+ end
106
+
107
+ telemetry
108
+ end
109
+
92
110
  # Adds the breadcrumb to the scope's breadcrumbs buffer.
93
111
  # @param breadcrumb [Breadcrumb]
94
112
  # @return [void]
@@ -117,6 +135,7 @@ module Sentry
117
135
  copy.session = session.deep_dup
118
136
  copy.propagation_context = propagation_context.deep_dup
119
137
  copy.attachments = attachments.dup
138
+ copy.event_processors = event_processors.dup
120
139
  copy
121
140
  end
122
141
 
@@ -278,6 +297,20 @@ module Sentry
278
297
  span
279
298
  end
280
299
 
300
+ # Returns the trace context for this scope.
301
+ # Prioritizes external propagation context (from OTel) over local propagation context.
302
+ # @return [Hash]
303
+ def get_trace_context
304
+ if span
305
+ span.get_trace_context.merge(dynamic_sampling_context: span.get_dynamic_sampling_context)
306
+ elsif (external_context = Sentry.get_external_propagation_context)
307
+ trace_id, span_id = external_context
308
+ { trace_id: trace_id, span_id: span_id }
309
+ else
310
+ propagation_context.get_trace_context.merge(dynamic_sampling_context: propagation_context.get_dynamic_sampling_context)
311
+ end
312
+ end
313
+
281
314
  # Sets the scope's fingerprint attribute.
282
315
  # @param fingerprint [Array]
283
316
  # @return [Array]
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Sequel
5
+ OP_NAME = "db.sql.sequel"
6
+ SPAN_ORIGIN = "auto.db.sequel"
7
+
8
+ # Sequel Database extension module that instruments queries
9
+ module DatabaseExtension
10
+ def log_connection_yield(sql, conn, args = nil)
11
+ return super unless Sentry.initialized?
12
+
13
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span|
14
+ result = super
15
+
16
+ if span
17
+ span.set_description(sql)
18
+ span.set_data(Span::DataConventions::DB_SYSTEM, database_type.to_s)
19
+ span.set_data(Span::DataConventions::DB_NAME, opts[:database]) if opts[:database]
20
+ span.set_data(Span::DataConventions::SERVER_ADDRESS, opts[:host]) if opts[:host]
21
+ span.set_data(Span::DataConventions::SERVER_PORT, opts[:port]) if opts[:port]
22
+ end
23
+
24
+ result
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ ::Sequel::Database.register_extension(:sentry, Sentry::Sequel::DatabaseExtension)
31
+ end
32
+
33
+ Sentry.register_patch(:sequel) do
34
+ ::Sequel::Database.extension(:sentry) if defined?(::Sequel::Database)
35
+ end
data/lib/sentry/span.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require "sentry/metrics/local_aggregator"
5
4
  require "sentry/utils/uuid"
6
5
 
7
6
  module Sentry
@@ -50,6 +49,9 @@ module Sentry
50
49
  MESSAGING_DESTINATION_NAME = "messaging.destination.name"
51
50
  MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency"
52
51
  MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count"
52
+
53
+ # Time in ms the request spent in the server queue before processing began.
54
+ HTTP_QUEUE_TIME_MS = "http.server.request.time_in_queue"
53
55
  end
54
56
 
55
57
  STATUS_MAP = {
@@ -173,8 +175,8 @@ module Sentry
173
175
  end
174
176
 
175
177
  # @return [Hash]
176
- def to_hash
177
- hash = {
178
+ def to_h
179
+ {
178
180
  trace_id: @trace_id,
179
181
  span_id: @span_id,
180
182
  parent_span_id: @parent_span_id,
@@ -187,11 +189,6 @@ module Sentry
187
189
  data: @data,
188
190
  origin: @origin
189
191
  }
190
-
191
- summary = metrics_summary
192
- hash[:_metrics_summary] = summary if summary
193
-
194
- hash
195
192
  end
196
193
 
197
194
  # Returns the span's context that can be used to embed in an Event.
@@ -307,14 +304,5 @@ module Sentry
307
304
  def set_origin(origin)
308
305
  @origin = origin
309
306
  end
310
-
311
- # Collects gauge metrics on the span for metric summaries.
312
- def metrics_local_aggregator
313
- @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new
314
- end
315
-
316
- def metrics_summary
317
- @metrics_local_aggregator&.to_hash
318
- end
319
307
  end
320
308
  end
@@ -37,6 +37,10 @@ module Sentry
37
37
  message = message.to_s.strip
38
38
 
39
39
  if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
40
+ if (filter = Sentry.configuration.std_lib_logger_filter) && !filter.call(self, message, method)
41
+ return result
42
+ end
43
+
40
44
  Sentry.logger.send(method, message, origin: ORIGIN)
41
45
  end
42
46
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/threaded_periodic_worker"
4
+ require "sentry/envelope"
5
+
6
+ module Sentry
7
+ # TelemetryEventBuffer is a base class for buffering telemetry events (logs, metrics, etc.)
8
+ # and sending them to Sentry in a single envelope.
9
+ #
10
+ # This is used internally by the `Sentry::Client`.
11
+ #
12
+ # @!visibility private
13
+ class TelemetryEventBuffer < ThreadedPeriodicWorker
14
+ FLUSH_INTERVAL = 5 # seconds
15
+
16
+ # @!visibility private
17
+ attr_reader :pending_items, :envelope_type, :data_category, :thread
18
+
19
+ def initialize(configuration, client, event_class:, max_items:, max_items_before_drop:, envelope_type:, envelope_content_type:, before_send:)
20
+ super(configuration.sdk_logger, FLUSH_INTERVAL)
21
+
22
+ @client = client
23
+ @dsn = configuration.dsn
24
+ @debug = configuration.debug
25
+ @event_class = event_class
26
+ @max_items = max_items
27
+ @max_items_before_drop = max_items_before_drop
28
+ @envelope_type = envelope_type
29
+ @data_category = Sentry::Envelope::Item.data_category(@envelope_type)
30
+ @envelope_content_type = envelope_content_type
31
+ @before_send = before_send
32
+
33
+ @pending_items = []
34
+ @mutex = Mutex.new
35
+
36
+ log_debug("[#{self.class}] Initialized buffer with max_items=#{@max_items}, flush_interval=#{FLUSH_INTERVAL}s")
37
+ end
38
+
39
+ def flush
40
+ @mutex.synchronize do
41
+ return if empty?
42
+
43
+ log_debug("[#{self.class}] flushing #{size} #{@event_class}")
44
+
45
+ send_items
46
+ end
47
+
48
+ self
49
+ end
50
+ alias_method :run, :flush
51
+
52
+ def add_item(item)
53
+ @mutex.synchronize do
54
+ return unless ensure_thread
55
+
56
+ if size >= @max_items_before_drop
57
+ log_debug("[#{self.class}] exceeded max capacity, dropping event")
58
+ @client.transport.record_lost_event(:queue_overflow, @data_category)
59
+ else
60
+ @pending_items << item
61
+ end
62
+
63
+ send_items if size >= @max_items
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ def empty?
70
+ @pending_items.empty?
71
+ end
72
+
73
+ def size
74
+ @pending_items.size
75
+ end
76
+
77
+ def clear!
78
+ @pending_items.clear
79
+ end
80
+
81
+ private
82
+
83
+ def send_items
84
+ envelope = Envelope.new(
85
+ event_id: Sentry::Utils.uuid,
86
+ sent_at: Sentry.utc_now.iso8601,
87
+ dsn: @dsn,
88
+ sdk: Sentry.sdk_meta
89
+ )
90
+
91
+ discarded_count = 0
92
+ envelope_items = []
93
+
94
+ if @before_send
95
+ @pending_items.each do |item|
96
+ processed_item = @before_send.call(item)
97
+
98
+ if processed_item
99
+ envelope_items << processed_item.to_h
100
+ else
101
+ discarded_count += 1
102
+ end
103
+ end
104
+ else
105
+ envelope_items = @pending_items.map(&:to_h)
106
+ end
107
+
108
+ unless discarded_count.zero?
109
+ @client.transport.record_lost_event(:before_send, @data_category, num: discarded_count)
110
+ end
111
+
112
+ return if envelope_items.empty?
113
+
114
+ envelope.add_item(
115
+ {
116
+ type: @envelope_type,
117
+ item_count: envelope_items.size,
118
+ content_type: @envelope_content_type
119
+ },
120
+ { items: envelope_items }
121
+ )
122
+
123
+ @client.send_envelope(envelope)
124
+ rescue => e
125
+ log_error("[#{self.class}] Failed to send #{@event_class}", e, debug: @debug)
126
+ ensure
127
+ clear!
128
+ end
129
+ end
130
+ end
@@ -27,6 +27,7 @@ module Sentry
27
27
  # set transport to DummyTransport, so we can easily intercept the captured events
28
28
  dummy_config.transport.transport_class = Sentry::DummyTransport
29
29
  # make sure SDK allows sending under the current environment
30
+ dummy_config.enabled_environments ||= []
30
31
  dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment)
31
32
  # disble async event sending
32
33
  dummy_config.background_worker_threads = 0
@@ -101,6 +102,13 @@ module Sentry
101
102
  .flat_map { |item| item.payload[:items] }
102
103
  end
103
104
 
105
+ def sentry_metrics
106
+ sentry_envelopes
107
+ .flat_map(&:items)
108
+ .select { |item| item.headers[:type] == "trace_metric" }
109
+ .flat_map { |item| item.payload[:items] }
110
+ end
111
+
104
112
  # Returns the last captured event object.
105
113
  # @return [Event, nil]
106
114
  def last_sentry_event