sentry-ruby 5.26.0 → 6.3.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +26 -4
  3. data/README.md +2 -2
  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/breadcrumb.rb +1 -1
  8. data/lib/sentry/breadcrumb_buffer.rb +2 -2
  9. data/lib/sentry/check_in_event.rb +2 -2
  10. data/lib/sentry/client.rb +59 -136
  11. data/lib/sentry/configuration.rb +168 -78
  12. data/lib/sentry/cron/monitor_check_ins.rb +3 -3
  13. data/lib/sentry/cron/monitor_config.rb +2 -2
  14. data/lib/sentry/cron/monitor_schedule.rb +2 -2
  15. data/lib/sentry/debug_structured_logger.rb +94 -0
  16. data/lib/sentry/dsn.rb +32 -0
  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/graphql.rb +1 -1
  21. data/lib/sentry/hub.rb +29 -5
  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 +33 -138
  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 -42
  34. data/lib/sentry/profiler.rb +4 -5
  35. data/lib/sentry/propagation_context.rb +55 -18
  36. data/lib/sentry/rspec.rb +1 -1
  37. data/lib/sentry/scope.rb +32 -5
  38. data/lib/sentry/sequel.rb +35 -0
  39. data/lib/sentry/span.rb +2 -17
  40. data/lib/sentry/std_lib_logger.rb +10 -1
  41. data/lib/sentry/telemetry_event_buffer.rb +130 -0
  42. data/lib/sentry/test_helper.rb +30 -0
  43. data/lib/sentry/transaction.rb +72 -95
  44. data/lib/sentry/transaction_event.rb +4 -9
  45. data/lib/sentry/transport/debug_transport.rb +70 -0
  46. data/lib/sentry/transport/dummy_transport.rb +1 -0
  47. data/lib/sentry/transport/http_transport.rb +9 -5
  48. data/lib/sentry/transport.rb +3 -5
  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/sample_rand.rb +97 -0
  52. data/lib/sentry/utils/telemetry_attributes.rb +30 -0
  53. data/lib/sentry/vernier/profiler.rb +4 -3
  54. data/lib/sentry/version.rb +1 -1
  55. data/lib/sentry-ruby.rb +25 -30
  56. data/sentry-ruby-core.gemspec +1 -1
  57. data/sentry-ruby.gemspec +1 -1
  58. metadata +17 -17
  59. data/lib/sentry/metrics/aggregator.rb +0 -248
  60. data/lib/sentry/metrics/configuration.rb +0 -47
  61. data/lib/sentry/metrics/counter_metric.rb +0 -25
  62. data/lib/sentry/metrics/distribution_metric.rb +0 -25
  63. data/lib/sentry/metrics/gauge_metric.rb +0 -35
  64. data/lib/sentry/metrics/local_aggregator.rb +0 -53
  65. data/lib/sentry/metrics/metric.rb +0 -19
  66. data/lib/sentry/metrics/set_metric.rb +0 -28
  67. data/lib/sentry/metrics/timing.rb +0 -51
@@ -1,55 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sentry/metrics/metric"
4
- require "sentry/metrics/counter_metric"
5
- require "sentry/metrics/distribution_metric"
6
- require "sentry/metrics/gauge_metric"
7
- require "sentry/metrics/set_metric"
8
- require "sentry/metrics/timing"
9
- require "sentry/metrics/aggregator"
3
+ require "sentry/metric_event"
10
4
 
11
5
  module Sentry
12
6
  module Metrics
13
- DURATION_UNITS = %w[nanosecond microsecond millisecond second minute hour day week]
14
- INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte]
15
- FRACTIONAL_UNITS = %w[ratio percent]
16
-
17
- OP_NAME = "metric.timing"
18
- SPAN_ORIGIN = "auto.metric.timing"
19
-
20
7
  class << self
21
- def increment(key, value = 1.0, unit: "none", tags: {}, timestamp: nil)
22
- Sentry.metrics_aggregator&.add(:c, key, value, unit: unit, tags: tags, timestamp: timestamp)
23
- end
24
-
25
- def distribution(key, value, unit: "none", tags: {}, timestamp: nil)
26
- Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)
27
- end
28
-
29
- def set(key, value, unit: "none", tags: {}, timestamp: nil)
30
- Sentry.metrics_aggregator&.add(:s, key, value, unit: unit, tags: tags, timestamp: timestamp)
8
+ # Increments a counter metric
9
+ # @param name [String] the metric name
10
+ # @param value [Numeric] the value to increment by (default: 1)
11
+ # @param attributes [Hash, nil] additional attributes for the metric (optional)
12
+ # @return [void]
13
+ def count(name, value: 1, attributes: nil)
14
+ return unless Sentry.initialized?
15
+
16
+ Sentry.get_current_hub.capture_metric(
17
+ name: name,
18
+ type: :counter,
19
+ value: value,
20
+ attributes: attributes
21
+ )
31
22
  end
32
23
 
33
- def gauge(key, value, unit: "none", tags: {}, timestamp: nil)
34
- Sentry.metrics_aggregator&.add(:g, key, value, unit: unit, tags: tags, timestamp: timestamp)
24
+ # Records a gauge metric
25
+ # @param name [String] the metric name
26
+ # @param value [Numeric] the gauge value
27
+ # @param unit [String, nil] the metric unit (optional)
28
+ # @param attributes [Hash, nil] additional attributes for the metric (optional)
29
+ # @return [void]
30
+ def gauge(name, value, unit: nil, attributes: nil)
31
+ return unless Sentry.initialized?
32
+
33
+ Sentry.get_current_hub.capture_metric(
34
+ name: name,
35
+ type: :gauge,
36
+ value: value,
37
+ unit: unit,
38
+ attributes: attributes
39
+ )
35
40
  end
36
41
 
37
- def timing(key, unit: "second", tags: {}, timestamp: nil, &block)
38
- return unless block_given?
39
- return yield unless DURATION_UNITS.include?(unit)
40
-
41
- result, value = Sentry.with_child_span(op: OP_NAME, description: key, origin: SPAN_ORIGIN) do |span|
42
- tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(", ") : v.to_s) } if span
43
-
44
- start = Timing.send(unit.to_sym)
45
- result = yield
46
- value = Timing.send(unit.to_sym) - start
47
-
48
- [result, value]
49
- end
50
-
51
- Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp)
52
- result
42
+ # Records a distribution metric
43
+ # @param name [String] the metric name
44
+ # @param value [Numeric] the distribution value
45
+ # @param unit [String, nil] the metric unit (optional)
46
+ # @param attributes [Hash, nil] additional attributes for the metric (optional)
47
+ # @return [void]
48
+ def distribution(name, value, unit: nil, attributes: nil)
49
+ return unless Sentry.initialized?
50
+
51
+ Sentry.get_current_hub.capture_metric(
52
+ name: name,
53
+ type: :distribution,
54
+ value: value,
55
+ unit: unit,
56
+ attributes: attributes
57
+ )
53
58
  end
54
59
  end
55
60
  end
@@ -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
 
@@ -3,15 +3,14 @@
3
3
  require "securerandom"
4
4
  require "sentry/baggage"
5
5
  require "sentry/utils/uuid"
6
+ require "sentry/utils/sample_rand"
6
7
 
7
8
  module Sentry
8
9
  class PropagationContext
9
10
  SENTRY_TRACE_REGEXP = Regexp.new(
10
- "^[ \t]*" + # whitespace
11
- "([0-9a-f]{32})?" + # trace_id
11
+ "\\A([0-9a-f]{32})?" + # trace_id
12
12
  "-?([0-9a-f]{16})?" + # span_id
13
- "-?([01])?" + # sampled
14
- "[ \t]*$" # whitespace
13
+ "-?([01])?\\z" # sampled
15
14
  )
16
15
 
17
16
  # An uuid that can be used to identify a trace.
@@ -33,6 +32,53 @@ module Sentry
33
32
  # Please use the #get_baggage method for interfacing outside this class.
34
33
  # @return [Baggage, nil]
35
34
  attr_reader :baggage
35
+ # The propagated random value used for sampling decisions.
36
+ # @return [Float, nil]
37
+ attr_reader :sample_rand
38
+
39
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
40
+ #
41
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
42
+ # @return [Array, nil]
43
+ def self.extract_sentry_trace(sentry_trace)
44
+ value = sentry_trace.to_s.strip
45
+ return if value.empty?
46
+
47
+ match = SENTRY_TRACE_REGEXP.match(value)
48
+ return if match.nil?
49
+
50
+ trace_id, parent_span_id, sampled_flag = match[1..3]
51
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
52
+
53
+ [trace_id, parent_span_id, parent_sampled]
54
+ end
55
+
56
+ def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
57
+ return unless baggage&.items
58
+
59
+ sample_rand_str = baggage.items["sample_rand"]
60
+ return unless sample_rand_str
61
+
62
+ generator = Utils::SampleRand.new(trace_id: trace_id)
63
+ generator.generate_from_value(sample_rand_str)
64
+ end
65
+
66
+ def self.generate_sample_rand(baggage, trace_id, parent_sampled)
67
+ generator = Utils::SampleRand.new(trace_id: trace_id)
68
+
69
+ if baggage&.items && !parent_sampled.nil?
70
+ sample_rate_str = baggage.items["sample_rate"]
71
+ sample_rate = sample_rate_str&.to_f
72
+
73
+ if sample_rate && !parent_sampled.nil?
74
+ generator.generate_from_sampling_decision(parent_sampled, sample_rate)
75
+ else
76
+ generator.generate_from_trace_id
77
+ end
78
+ else
79
+ generator.generate_from_trace_id
80
+ end
81
+ end
36
82
 
37
83
  def initialize(scope, env = nil)
38
84
  @scope = scope
@@ -40,6 +86,7 @@ module Sentry
40
86
  @parent_sampled = nil
41
87
  @baggage = nil
42
88
  @incoming_trace = false
89
+ @sample_rand = nil
43
90
 
44
91
  if env
45
92
  sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
@@ -61,6 +108,8 @@ module Sentry
61
108
  Baggage.new({})
62
109
  end
63
110
 
111
+ @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
112
+
64
113
  @baggage.freeze!
65
114
  @incoming_trace = true
66
115
  end
@@ -69,20 +118,7 @@ module Sentry
69
118
 
70
119
  @trace_id ||= Utils.uuid
71
120
  @span_id = Utils.uuid.slice(0, 16)
72
- end
73
-
74
- # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
75
- #
76
- # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
77
- # @return [Array, nil]
78
- def self.extract_sentry_trace(sentry_trace)
79
- match = SENTRY_TRACE_REGEXP.match(sentry_trace)
80
- return nil if match.nil?
81
-
82
- trace_id, parent_span_id, sampled_flag = match[1..3]
83
- parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
84
-
85
- [trace_id, parent_span_id, parent_sampled]
121
+ @sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
86
122
  end
87
123
 
88
124
  # Returns the trace context that can be used to embed in an Event.
@@ -123,6 +159,7 @@ module Sentry
123
159
 
124
160
  items = {
125
161
  "trace_id" => trace_id,
162
+ "sample_rand" => Utils::SampleRand.format(@sample_rand),
126
163
  "environment" => configuration.environment,
127
164
  "release" => configuration.release,
128
165
  "public_key" => configuration.dsn&.public_key
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,10 +60,6 @@ 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
63
  if span
68
64
  event.contexts[:trace] ||= span.get_trace_context
69
65
 
@@ -89,6 +85,37 @@ module Sentry
89
85
  event
90
86
  end
91
87
 
88
+ # A leaner version of apply_to_event that applies to
89
+ # lightweight payloads like Logs and Metrics.
90
+ #
91
+ # Adds trace_id, span_id, user from the scope and default attributes from configuration.
92
+ #
93
+ # @param telemetry [MetricEvent, LogEvent] the telemetry event to apply scope context to
94
+ # @return [MetricEvent, LogEvent] the telemetry event with scope context applied
95
+ def apply_to_telemetry(telemetry)
96
+ # TODO-neel when new scope set_attribute api is added: add them here
97
+ trace_context = span ? span.get_trace_context : propagation_context.get_trace_context
98
+ telemetry.trace_id = trace_context[:trace_id]
99
+ telemetry.span_id = trace_context[:span_id]
100
+
101
+ configuration = Sentry.configuration
102
+ return telemetry unless configuration
103
+
104
+ telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"]
105
+ telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"]
106
+ telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment
107
+ telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
108
+ telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
109
+
110
+ if configuration.send_default_pii && !user.empty?
111
+ telemetry.attributes["user.id"] ||= user[:id] if user[:id]
112
+ telemetry.attributes["user.name"] ||= user[:username] if user[:username]
113
+ telemetry.attributes["user.email"] ||= user[:email] if user[:email]
114
+ end
115
+
116
+ telemetry
117
+ end
118
+
92
119
  # Adds the breadcrumb to the scope's breadcrumbs buffer.
93
120
  # @param breadcrumb [Breadcrumb]
94
121
  # @return [void]
@@ -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
@@ -173,8 +172,8 @@ module Sentry
173
172
  end
174
173
 
175
174
  # @return [Hash]
176
- def to_hash
177
- hash = {
175
+ def to_h
176
+ {
178
177
  trace_id: @trace_id,
179
178
  span_id: @span_id,
180
179
  parent_span_id: @parent_span_id,
@@ -187,11 +186,6 @@ module Sentry
187
186
  data: @data,
188
187
  origin: @origin
189
188
  }
190
-
191
- summary = metrics_summary
192
- hash[:_metrics_summary] = summary if summary
193
-
194
- hash
195
189
  end
196
190
 
197
191
  # Returns the span's context that can be used to embed in an Event.
@@ -307,14 +301,5 @@ module Sentry
307
301
  def set_origin(origin)
308
302
  @origin = origin
309
303
  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
304
  end
320
305
  end
@@ -12,11 +12,16 @@ module Sentry
12
12
  4 => :fatal
13
13
  }.freeze
14
14
 
15
+ ORIGIN = "auto.log.ruby.std_logger"
16
+
15
17
  def add(severity, message = nil, progname = nil, &block)
16
18
  result = super
17
19
 
18
20
  return unless Sentry.initialized? && Sentry.get_current_hub
19
21
 
22
+ # Only process logs that meet or exceed the logger's level
23
+ return result if severity < level
24
+
20
25
  # exclude sentry SDK logs -- to prevent recursive log action,
21
26
  # do not process internal logs again
22
27
  if message.nil? && progname != Sentry::Logger::PROGNAME
@@ -32,7 +37,11 @@ module Sentry
32
37
  message = message.to_s.strip
33
38
 
34
39
  if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
35
- Sentry.logger.send(method, message)
40
+ if (filter = Sentry.configuration.std_lib_logger_filter) && !filter.call(self, message, method)
41
+ return result
42
+ end
43
+
44
+ Sentry.logger.send(method, message, origin: ORIGIN)
36
45
  end
37
46
  end
38
47
 
@@ -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
@@ -2,8 +2,13 @@
2
2
 
3
3
  module Sentry
4
4
  module TestHelper
5
+ module_function
6
+
5
7
  DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
6
8
 
9
+ # Not really real, but it will be resolved as a non-local for testing needs
10
+ REAL_DSN = "https://user:pass@getsentry.io/project/42"
11
+
7
12
  # Alters the existing SDK configuration with test-suitable options. Mainly:
8
13
  # - Sets a dummy DSN instead of `nil` or an actual DSN.
9
14
  # - Sets the transport to DummyTransport, which allows easy access to the captured events.
@@ -22,6 +27,7 @@ module Sentry
22
27
  # set transport to DummyTransport, so we can easily intercept the captured events
23
28
  dummy_config.transport.transport_class = Sentry::DummyTransport
24
29
  # make sure SDK allows sending under the current environment
30
+ dummy_config.enabled_environments ||= []
25
31
  dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment)
26
32
  # disble async event sending
27
33
  dummy_config.background_worker_threads = 0
@@ -46,6 +52,8 @@ module Sentry
46
52
  def teardown_sentry_test
47
53
  return unless Sentry.initialized?
48
54
 
55
+ clear_sentry_events
56
+
49
57
  # pop testing layer created by `setup_sentry_test`
50
58
  # but keep the base layer to avoid nil-pointer errors
51
59
  # TODO: find a way to notify users if they somehow popped the test layer before calling this method
@@ -55,6 +63,21 @@ module Sentry
55
63
  Sentry::Scope.global_event_processors.clear
56
64
  end
57
65
 
66
+ def clear_sentry_events
67
+ return unless Sentry.initialized?
68
+
69
+ sentry_transport.clear if sentry_transport.respond_to?(:clear)
70
+
71
+ if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear)
72
+ sentry_logger.clear
73
+ end
74
+ end
75
+
76
+ # @return [Sentry::StructuredLogger, Sentry::DebugStructuredLogger]
77
+ def sentry_logger
78
+ Sentry.logger
79
+ end
80
+
58
81
  # @return [Transport]
59
82
  def sentry_transport
60
83
  Sentry.get_current_client.transport
@@ -79,6 +102,13 @@ module Sentry
79
102
  .flat_map { |item| item.payload[:items] }
80
103
  end
81
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
+
82
112
  # Returns the last captured event object.
83
113
  # @return [Event, nil]
84
114
  def last_sentry_event