sentry-ruby 7.0.0 → 7.2.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -0
  3. data/gemfiles/ruby-2.7_rack-2_redis-4.gemfile.lock +21 -9
  4. data/gemfiles/ruby-2.7_rack-3.1_redis-4.gemfile.lock +21 -9
  5. data/gemfiles/ruby-2.7_rack-3_redis-4.gemfile.lock +21 -9
  6. data/gemfiles/ruby-3.0_rack-2_redis-4.gemfile.lock +30 -14
  7. data/gemfiles/ruby-3.0_rack-3.1_redis-4.gemfile.lock +30 -14
  8. data/gemfiles/ruby-3.0_rack-3_redis-4.gemfile.lock +30 -14
  9. data/gemfiles/ruby-3.1_rack-2_redis-4.gemfile.lock +50 -27
  10. data/gemfiles/ruby-3.1_rack-3.1_redis-4.gemfile.lock +50 -27
  11. data/gemfiles/ruby-3.1_rack-3_redis-4.gemfile.lock +50 -27
  12. data/gemfiles/ruby-3.2_rack-0_redis-5.gemfile.lock +54 -31
  13. data/gemfiles/ruby-3.2_rack-2_redis-4.gemfile.lock +52 -29
  14. data/gemfiles/ruby-3.2_rack-2_redis-5.gemfile.lock +54 -31
  15. data/gemfiles/ruby-3.2_rack-3.1_redis-4.gemfile.lock +52 -29
  16. data/gemfiles/ruby-3.2_rack-3_redis-4.gemfile.lock +52 -29
  17. data/gemfiles/ruby-3.2_rack-3_redis-5.gemfile.lock +54 -31
  18. data/gemfiles/ruby-3.3_rack-2_redis-4.gemfile.lock +56 -33
  19. data/gemfiles/ruby-3.3_rack-3.1_redis-4.gemfile.lock +56 -33
  20. data/gemfiles/ruby-3.3_rack-3.1_redis-5.3.gemfile.lock +58 -35
  21. data/gemfiles/ruby-3.3_rack-3_redis-4.gemfile.lock +56 -33
  22. data/gemfiles/ruby-3.4_rack-2_redis-4.gemfile.lock +56 -33
  23. data/gemfiles/ruby-3.4_rack-3.1_redis-4.gemfile.lock +56 -33
  24. data/gemfiles/ruby-3.4_rack-3.1_redis-5.3.gemfile.lock +58 -35
  25. data/gemfiles/ruby-3.4_rack-3_redis-4.gemfile.lock +56 -33
  26. data/gemfiles/ruby-4.0_rack-2_redis-4.gemfile.lock +56 -33
  27. data/gemfiles/ruby-4.0_rack-3.1_redis-4.gemfile.lock +56 -33
  28. data/gemfiles/ruby-4.0_rack-3_redis-4.gemfile.lock +56 -33
  29. data/gemfiles/ruby-jruby-9.4.14.0_rack-2_redis-4.gemfile.lock +59 -41
  30. data/gemfiles/ruby-jruby-9.4.14.0_rack-3.1_redis-4.gemfile.lock +59 -41
  31. data/gemfiles/ruby-jruby-9.4.14.0_rack-3_redis-4.gemfile.lock +59 -41
  32. data/lib/sentry/backtrace.rb +10 -1
  33. data/lib/sentry/client.rb +19 -6
  34. data/lib/sentry/configuration.rb +8 -3
  35. data/lib/sentry/cron/fugit_schedule.rb +117 -0
  36. data/lib/sentry/cron/monitor_check_ins.rb +33 -27
  37. data/lib/sentry/cron/slug.rb +18 -0
  38. data/lib/sentry/data_collection.rb +6 -1
  39. data/lib/sentry/faraday.rb +4 -1
  40. data/lib/sentry/hub.rb +9 -2
  41. data/lib/sentry/integrable.rb +12 -0
  42. data/lib/sentry/log_event_buffer.rb +1 -1
  43. data/lib/sentry/metric_event.rb +4 -2
  44. data/lib/sentry/metric_event_buffer.rb +1 -1
  45. data/lib/sentry/metrics.rb +12 -6
  46. data/lib/sentry/rufus_scheduler.rb +120 -0
  47. data/lib/sentry/scope.rb +7 -3
  48. data/lib/sentry/session_flusher.rb +7 -0
  49. data/lib/sentry/std_lib_logger.rb +8 -2
  50. data/lib/sentry/telemetry_event_buffer.rb +57 -31
  51. data/lib/sentry/test_helper.rb +4 -0
  52. data/lib/sentry/threaded_periodic_worker.rb +90 -13
  53. data/lib/sentry/transaction.rb +7 -1
  54. data/lib/sentry/transport/debug_transport.rb +11 -2
  55. data/lib/sentry/transport/dummy_transport.rb +7 -4
  56. data/lib/sentry/transport/spotlight_transport.rb +2 -2
  57. data/lib/sentry/utils/callback_helper.rb +27 -0
  58. data/lib/sentry/version.rb +1 -1
  59. data/lib/sentry-ruby.rb +6 -0
  60. metadata +10 -6
@@ -32,5 +32,17 @@ module Sentry
32
32
  options[:hint][:integration] = integration_name
33
33
  Sentry.capture_check_in(slug, status, **options, &block)
34
34
  end
35
+
36
+ def count(name, value: 1, attributes: nil)
37
+ Sentry.metrics.count(name, value: value, attributes: attributes, integration: integration_name)
38
+ end
39
+
40
+ def gauge(name, value, unit: nil, attributes: nil)
41
+ Sentry.metrics.gauge(name, value, unit: unit, attributes: attributes, integration: integration_name)
42
+ end
43
+
44
+ def distribution(name, value, unit: nil, attributes: nil)
45
+ Sentry.metrics.distribution(name, value, unit: unit, attributes: attributes, integration: integration_name)
46
+ end
35
47
  end
36
48
  end
@@ -21,7 +21,7 @@ module Sentry
21
21
  max_items_before_drop: MAX_EVENTS_BEFORE_DROP,
22
22
  envelope_type: "log",
23
23
  envelope_content_type: "application/vnd.sentry.items.log+json",
24
- before_send: configuration.before_send_log
24
+ before_send: :before_send_log
25
25
  )
26
26
  end
27
27
  end
@@ -6,7 +6,7 @@ module Sentry
6
6
  class MetricEvent
7
7
  include Sentry::Utils::TelemetryAttributes
8
8
 
9
- attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes
9
+ attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes, :integration_meta
10
10
  attr_writer :trace_id, :span_id, :attributes
11
11
 
12
12
  def initialize(
@@ -14,13 +14,15 @@ module Sentry
14
14
  type:,
15
15
  value:,
16
16
  unit: nil,
17
- attributes: nil
17
+ attributes: nil,
18
+ integration_meta: nil
18
19
  )
19
20
  @name = name
20
21
  @type = type
21
22
  @value = value
22
23
  @unit = unit
23
24
  @attributes = attributes || {}
25
+ @integration_meta = integration_meta
24
26
 
25
27
  @timestamp = Sentry.utc_now
26
28
  @trace_id = nil
@@ -21,7 +21,7 @@ module Sentry
21
21
  max_items_before_drop: MAX_METRICS_BEFORE_DROP,
22
22
  envelope_type: "trace_metric",
23
23
  envelope_content_type: "application/vnd.sentry.items.trace-metric+json",
24
- before_send: configuration.before_send_metric
24
+ before_send: :before_send_metric
25
25
  )
26
26
  end
27
27
  end
@@ -9,15 +9,17 @@ module Sentry
9
9
  # @param name [String] the metric name
10
10
  # @param value [Numeric] the value to increment by (default: 1)
11
11
  # @param attributes [Hash, nil] additional attributes for the metric (optional)
12
+ # @param integration [String, Symbol, nil] registered integration emitting the metric (optional)
12
13
  # @return [void]
13
- def count(name, value: 1, attributes: nil)
14
+ def count(name, value: 1, attributes: nil, integration: nil)
14
15
  return unless Sentry.initialized?
15
16
 
16
17
  Sentry.get_current_hub.capture_metric(
17
18
  name: name,
18
19
  type: :counter,
19
20
  value: value,
20
- attributes: attributes
21
+ attributes: attributes,
22
+ integration: integration
21
23
  )
22
24
  end
23
25
 
@@ -26,8 +28,9 @@ module Sentry
26
28
  # @param value [Numeric] the gauge value
27
29
  # @param unit [String, nil] the metric unit (optional)
28
30
  # @param attributes [Hash, nil] additional attributes for the metric (optional)
31
+ # @param integration [String, Symbol, nil] registered integration emitting the metric (optional)
29
32
  # @return [void]
30
- def gauge(name, value, unit: nil, attributes: nil)
33
+ def gauge(name, value, unit: nil, attributes: nil, integration: nil)
31
34
  return unless Sentry.initialized?
32
35
 
33
36
  Sentry.get_current_hub.capture_metric(
@@ -35,7 +38,8 @@ module Sentry
35
38
  type: :gauge,
36
39
  value: value,
37
40
  unit: unit,
38
- attributes: attributes
41
+ attributes: attributes,
42
+ integration: integration
39
43
  )
40
44
  end
41
45
 
@@ -44,8 +48,9 @@ module Sentry
44
48
  # @param value [Numeric] the distribution value
45
49
  # @param unit [String, nil] the metric unit (optional)
46
50
  # @param attributes [Hash, nil] additional attributes for the metric (optional)
51
+ # @param integration [String, Symbol, nil] registered integration emitting the metric (optional)
47
52
  # @return [void]
48
- def distribution(name, value, unit: nil, attributes: nil)
53
+ def distribution(name, value, unit: nil, attributes: nil, integration: nil)
49
54
  return unless Sentry.initialized?
50
55
 
51
56
  Sentry.get_current_hub.capture_metric(
@@ -53,7 +58,8 @@ module Sentry
53
58
  type: :distribution,
54
59
  value: value,
55
60
  unit: unit,
56
- attributes: attributes
61
+ attributes: attributes,
62
+ integration: integration
57
63
  )
58
64
  end
59
65
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # Sends Crons check-ins for rufus-scheduler `cron`, `every` and `interval` jobs.
5
+ # Enable with `config.enabled_patches << :rufus_scheduler`.
6
+ module RufusScheduler
7
+ module Job
8
+ protected
9
+
10
+ def do_call(time, do_rescue)
11
+ return super unless Sentry.initialized? && (monitor = sentry_monitor)
12
+
13
+ slug, monitor_config = monitor
14
+ check_in_id = Sentry.capture_check_in(slug, :in_progress, monitor_config: monitor_config)
15
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+
17
+ begin
18
+ # Run without rufus's rescue so the error is seen, then handle it like Job#do_call.
19
+ ret = super(time, false)
20
+ rescue Exception => e
21
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
22
+ Sentry.capture_check_in(slug, :error, check_in_id: check_in_id, duration: duration, monitor_config: monitor_config)
23
+
24
+ raise unless do_rescue && e.is_a?(StandardError)
25
+ return if e.is_a?(::Rufus::Scheduler::Job::KillSignal)
26
+
27
+ return @scheduler.on_error(self, e)
28
+ end
29
+
30
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
31
+ Sentry.capture_check_in(slug, :ok, check_in_id: check_in_id, duration: duration, monitor_config: monitor_config)
32
+
33
+ ret
34
+ end
35
+
36
+ private
37
+
38
+ def sentry_monitor
39
+ return @sentry_monitor if defined?(@sentry_monitor)
40
+
41
+ @sentry_monitor = Sentry::RufusScheduler.monitor_for(self)
42
+ rescue StandardError => e
43
+ Sentry.sdk_logger.error(Sentry::LOGGER_PROGNAME) { "Not monitoring rufus-scheduler job #{id}: #{e.class}: #{e.message}" }
44
+ @sentry_monitor = nil
45
+ end
46
+ end
47
+
48
+ class << self
49
+ # Returns [slug, monitor_config], or nil for jobs that aren't monitored (like `at`/`in`).
50
+ def monitor_for(job)
51
+ return unless job.is_a?(::Rufus::Scheduler::RepeatJob)
52
+ return if job.opts[:sentry_monitor] == false
53
+ return if sidekiq_scheduler_job?(job)
54
+
55
+ slug = slug_for(job)
56
+ unless slug
57
+ Sentry.sdk_logger.warn(Sentry::LOGGER_PROGNAME) do
58
+ "Not monitoring rufus-scheduler job #{job.id} at #{job.source_location&.join(":")}: " \
59
+ "pass `name:` to give it a stable monitor slug."
60
+ end
61
+ return
62
+ end
63
+
64
+ [slug, monitor_config_for(job)]
65
+ end
66
+
67
+ private
68
+
69
+ # sidekiq-scheduler jobs are monitored by the :sidekiq_scheduler patch.
70
+ def sidekiq_scheduler_job?(job)
71
+ file, _ = job.callable.source_location if job.callable.respond_to?(:source_location)
72
+ file&.end_with?("sidekiq-scheduler/scheduler.rb")
73
+ end
74
+
75
+ # Job ids are random per process and block paths change between deploys,
76
+ # so the slug comes from `name:`, then the handler method or class.
77
+ def slug_for(job)
78
+ handler = job.handler
79
+ source =
80
+ if job.name
81
+ job.name.to_s
82
+ elsif handler.is_a?(Method)
83
+ # The receiver's class, not the method owner, so inherited and mixin methods get distinct slugs.
84
+ namespace = handler.receiver.is_a?(Module) ? handler.receiver : handler.receiver.class
85
+ [namespace == Object ? nil : namespace.name, handler.name].compact.join("::")
86
+ elsif !handler.is_a?(Proc)
87
+ handler.class.name
88
+ end
89
+ return unless source
90
+
91
+ Sentry::Cron::Slug.from(source)
92
+ end
93
+
94
+ def monitor_config_for(job)
95
+ case job
96
+ when ::Rufus::Scheduler::CronJob
97
+ Sentry::Cron::FugitSchedule.from_cron(job.cron_line)
98
+ when ::Rufus::Scheduler::EveryJob
99
+ Sentry::Cron::FugitSchedule.from_seconds(job.frequency)
100
+ end
101
+ # IntervalJob waits `interval` after each run ends, so it has no fixed
102
+ # schedule to monitor against.
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ Sentry.register_patch(:rufus_scheduler) do |config|
109
+ # Load rufus-scheduler in case it's required after Sentry.init.
110
+ begin
111
+ require "rufus-scheduler"
112
+ rescue LoadError
113
+ end
114
+
115
+ if defined?(::Rufus::Scheduler::RepeatJob)
116
+ ::Rufus::Scheduler::Job.prepend(Sentry::RufusScheduler::Job) unless ::Rufus::Scheduler::Job.ancestors.include?(Sentry::RufusScheduler::Job)
117
+ else
118
+ config.sdk_logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the rufus-scheduler integration but the `rufus-scheduler` gem was not detected." }
119
+ end
120
+ end
data/lib/sentry/scope.rb CHANGED
@@ -9,6 +9,7 @@ require "etc"
9
9
  module Sentry
10
10
  class Scope
11
11
  include ArgumentCheckingHelper
12
+ include CallbackHelper
12
13
 
13
14
  ATTRIBUTES = [
14
15
  :transaction_name,
@@ -71,7 +72,8 @@ module Sentry
71
72
 
72
73
  unless all_event_processors.empty?
73
74
  all_event_processors.each do |processor_block|
74
- event = processor_block.call(event, hint)
75
+ event = safe_dispatch_callback("event_processor", processor_block, [event, hint])
76
+ return unless event
75
77
  end
76
78
  end
77
79
 
@@ -102,8 +104,10 @@ module Sentry
102
104
  configuration = Sentry.configuration
103
105
  return telemetry unless configuration
104
106
 
105
- telemetry.attributes["sentry.sdk.name"] ||= Sentry.sdk_meta["name"]
106
- telemetry.attributes["sentry.sdk.version"] ||= Sentry.sdk_meta["version"]
107
+ sdk_meta = telemetry.respond_to?(:integration_meta) ? telemetry.integration_meta : nil
108
+ sdk_meta ||= Sentry.sdk_meta
109
+ telemetry.attributes["sentry.sdk.name"] ||= sdk_meta[:name] || sdk_meta["name"]
110
+ telemetry.attributes["sentry.sdk.version"] ||= sdk_meta[:version] || sdk_meta["version"]
107
111
  telemetry.attributes["sentry.environment"] ||= configuration.environment if configuration.environment
108
112
  telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
109
113
  telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
@@ -35,6 +35,13 @@ module Sentry
35
35
 
36
36
  private
37
37
 
38
+ def reset_if_forked
39
+ return unless super
40
+
41
+ @mutex = Mutex.new
42
+ @pending_aggregates = {}
43
+ end
44
+
38
45
  def init_aggregates(aggregation_key)
39
46
  aggregates = { started: aggregation_key.iso8601 }
40
47
  Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 }
@@ -4,6 +4,8 @@ module Sentry
4
4
  # Ruby Logger support Add commentMore actions
5
5
  # intercepts any logger instance and send the log to Sentry too.
6
6
  module StdLibLogger
7
+ include CallbackHelper
8
+
7
9
  SEVERITY_MAP = {
8
10
  0 => :debug,
9
11
  1 => :info,
@@ -37,8 +39,12 @@ module Sentry
37
39
  message = message.to_s.strip
38
40
 
39
41
  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
+ if filter = Sentry.configuration.std_lib_logger_filter
43
+ return result unless safe_dispatch_callback(
44
+ "std_lib_logger_filter",
45
+ filter,
46
+ [self, message, method]
47
+ )
42
48
  end
43
49
 
44
50
  Sentry.logger.send(method, message, origin: ORIGIN)
@@ -12,7 +12,10 @@ module Sentry
12
12
  #
13
13
  # @!visibility private
14
14
  class TelemetryEventBuffer < ThreadedPeriodicWorker
15
+ include CallbackHelper
16
+
15
17
  FLUSH_INTERVAL = 5 # seconds
18
+ FLUSH_TIMEOUT = 2 # seconds
16
19
 
17
20
  # @!visibility private
18
21
  attr_reader :pending_items, :envelope_type, :data_category, :thread
@@ -21,6 +24,7 @@ module Sentry
21
24
  super(configuration.sdk_logger, FLUSH_INTERVAL)
22
25
 
23
26
  @client = client
27
+ @configuration = configuration
24
28
  @dsn = configuration.dsn
25
29
  @debug = configuration.debug
26
30
  @event_class = event_class
@@ -38,39 +42,44 @@ module Sentry
38
42
  end
39
43
 
40
44
  def flush
41
- @mutex.synchronize do
42
- return if empty?
43
-
44
- log_debug("[#{self.class}] flushing #{size} #{@event_class}")
45
+ return unless thread&.alive? && wake
45
46
 
46
- send_items
47
- end
47
+ wait_until_idle(FLUSH_TIMEOUT)
48
+ end
48
49
 
49
- self
50
+ def run
51
+ flush_pending_items
50
52
  end
51
- alias_method :run, :flush
52
53
 
53
54
  def add_item(item)
55
+ # the buffer thread can never add telemetry itself to prevent recursion
56
+ return self if Thread.current == thread
57
+ return unless ensure_thread
58
+
54
59
  # Prevent ThreadError from re-entrant locking (e.g. transport instrumentation calling Sentry.metrics.*)
55
60
  return self if @mutex.owned?
56
61
 
57
- @mutex.synchronize do
58
- return unless ensure_thread
59
-
62
+ dropped = false
63
+ size_exceeded = @mutex.synchronize do
60
64
  if size >= @max_items_before_drop
61
- log_debug("[#{self.class}] exceeded max capacity, dropping event")
62
- @client.transport.record_lost_event(
63
- :queue_overflow,
64
- @data_category,
65
- num_bytes: JSON.generate(item.to_h).bytesize
66
- )
65
+ dropped = true
67
66
  else
68
67
  @pending_items << item
69
68
  end
70
69
 
71
- send_items if size >= @max_items
70
+ size >= @max_items
71
+ end
72
+
73
+ if dropped
74
+ log_debug("[#{self.class}] exceeded max capacity, dropping event")
75
+ @client.transport.record_lost_event(
76
+ :queue_overflow,
77
+ @data_category,
78
+ num_bytes: JSON.generate(item.to_h).bytesize
79
+ )
72
80
  end
73
81
 
82
+ wake if size_exceeded
74
83
  self
75
84
  end
76
85
 
@@ -88,21 +97,40 @@ module Sentry
88
97
 
89
98
  private
90
99
 
91
- def send_items
92
- envelope = Envelope.new(
93
- event_id: Sentry::Utils.uuid,
94
- sent_at: Sentry.utc_now.iso8601,
95
- dsn: @dsn,
96
- sdk: Sentry.sdk_meta
97
- )
100
+ def reset_if_forked
101
+ return unless super
102
+
103
+ # Discard items inherited from the parent to avoid duplicate delivery.
104
+ @mutex = Mutex.new
105
+ @pending_items = []
106
+ end
107
+
108
+ def flush_pending_items
109
+ pending_items = @mutex.synchronize do
110
+ next if @pending_items.empty?
111
+
112
+ items = @pending_items
113
+ @pending_items = []
114
+ items
115
+ end
116
+
117
+ return unless pending_items
118
+
119
+ log_debug("[#{self.class}] flushing #{pending_items.size} #{@event_class}")
120
+ send_items(pending_items)
121
+ self
122
+ end
123
+
124
+ def send_items(pending_items)
125
+ envelope = Envelope.new(sent_at: Sentry.utc_now.iso8601)
98
126
 
99
127
  discarded_count = 0
100
128
  discarded_bytes = 0
101
129
  envelope_items = []
102
130
 
103
- if @before_send
104
- @pending_items.each do |item|
105
- processed_item = @before_send.call(item)
131
+ if callback = @configuration.send(@before_send)
132
+ pending_items.each do |item|
133
+ processed_item = safe_dispatch_callback(@before_send.to_s, callback, [item])
106
134
 
107
135
  if processed_item
108
136
  envelope_items << processed_item.to_h
@@ -112,7 +140,7 @@ module Sentry
112
140
  end
113
141
  end
114
142
  else
115
- envelope_items = @pending_items.map(&:to_h)
143
+ envelope_items = pending_items.map(&:to_h)
116
144
  end
117
145
 
118
146
  unless discarded_count.zero?
@@ -133,8 +161,6 @@ module Sentry
133
161
  @client.send_envelope(envelope)
134
162
  rescue => e
135
163
  log_error("[#{self.class}] Failed to send #{@event_class}", e, debug: @debug)
136
- ensure
137
- clear!
138
164
  end
139
165
  end
140
166
  end
@@ -127,6 +127,8 @@ module Sentry
127
127
  end
128
128
 
129
129
  def sentry_logs
130
+ Sentry.get_current_client&.log_event_buffer&.flush
131
+
130
132
  sentry_envelopes
131
133
  .flat_map(&:items)
132
134
  .select { |item| item.headers[:type] == "log" }
@@ -134,6 +136,8 @@ module Sentry
134
136
  end
135
137
 
136
138
  def sentry_metrics
139
+ Sentry.get_current_client&.metric_event_buffer&.flush
140
+
137
141
  sentry_envelopes
138
142
  .flat_map(&:items)
139
143
  .select { |item| item.headers[:type] == "trace_metric" }
@@ -4,41 +4,118 @@ module Sentry
4
4
  class ThreadedPeriodicWorker
5
5
  include LoggingHelper
6
6
 
7
+ # @!visibility private
8
+ attr_reader :thread, :thread_mutex, :idle_condition
9
+
7
10
  def initialize(sdk_logger, interval)
11
+ @process_id = Process.pid
8
12
  @thread = nil
9
13
  @exited = false
10
14
  @interval = interval
11
15
  @sdk_logger = sdk_logger
16
+
17
+ @thread_mutex = Mutex.new
18
+ @wake_condition = ConditionVariable.new
19
+ @idle_condition = ConditionVariable.new
20
+
21
+ @woken = false
22
+ @running = false
12
23
  end
13
24
 
14
25
  def ensure_thread
15
- return false if @exited
16
- return true if @thread&.alive?
26
+ reset_if_forked
17
27
 
18
- @thread = Thread.new do
19
- loop do
20
- sleep(@interval)
21
- run
22
- end
23
- end
28
+ @thread_mutex.synchronize do
29
+ return false if @exited
30
+ return true if @thread&.alive?
24
31
 
25
- true
32
+ @thread = Thread.new { worker_loop }
33
+
34
+ true
35
+ end
26
36
  rescue ThreadError
37
+ @thread_mutex.synchronize { @exited = true }
27
38
  log_debug("[#{self.class.name}] thread creation failed")
28
- @exited = true
29
39
  false
30
40
  end
31
41
 
42
+ def wake
43
+ @thread_mutex.synchronize do
44
+ next false if @exited
45
+
46
+ @woken = true
47
+ @wake_condition.signal
48
+ true
49
+ end
50
+ end
51
+
52
+ def wait_until_idle(timeout = nil)
53
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout if timeout
54
+
55
+ @thread_mutex.synchronize do
56
+ while !@exited && (@running || @woken)
57
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) if deadline
58
+ return false if remaining && remaining <= 0
59
+
60
+ @idle_condition.wait(@thread_mutex, remaining)
61
+ end
62
+
63
+ true
64
+ end
65
+ end
66
+
32
67
  def kill
33
- @exited = true
68
+ thread = @thread_mutex.synchronize do
69
+ @exited = true
70
+ @woken = false
71
+ @idle_condition.broadcast
72
+ @thread
73
+ end
34
74
 
35
75
  # Only a started worker has a thread to kill (and to log about).
36
76
  # Guarding here keeps a never-started worker's teardown silent, so
37
77
  # killing one during test reset can't emit a stray debug line.
38
- return unless @thread
78
+ return unless thread
39
79
 
40
80
  log_debug("[#{self.class.name}] thread killed")
41
- @thread.kill
81
+ thread.kill
82
+ end
83
+
84
+ private
85
+
86
+ def reset_if_forked
87
+ return false if @process_id == Process.pid
88
+
89
+ @process_id = Process.pid
90
+ @thread = nil
91
+ @exited = false
92
+ @woken = false
93
+ @running = false
94
+ @thread_mutex = Mutex.new
95
+ @wake_condition = ConditionVariable.new
96
+ @idle_condition = ConditionVariable.new
97
+ true
98
+ end
99
+
100
+ def worker_loop
101
+ loop do
102
+ @thread_mutex.synchronize do
103
+ @wake_condition.wait(@thread_mutex, @interval) unless @woken
104
+ @woken = false
105
+ @running = true
106
+ end
107
+
108
+ begin
109
+ run
110
+ rescue Exception => e
111
+ log_error("[#{self.class.name}] run failed", e)
112
+ ensure
113
+ @thread_mutex.synchronize do
114
+ @running = false
115
+ @idle_condition.broadcast
116
+ end
117
+ end
118
+ end
42
119
  end
43
120
  end
44
121
  end
@@ -14,6 +14,7 @@ module Sentry
14
14
  SOURCES = %i[custom url route view component task]
15
15
 
16
16
  include LoggingHelper
17
+ include CallbackHelper
17
18
 
18
19
  # The name of the transaction.
19
20
  # @return [String]
@@ -142,7 +143,12 @@ module Sentry
142
143
 
143
144
  sample_rate =
144
145
  if configuration.traces_sampler.is_a?(Proc)
145
- configuration.traces_sampler.call(sampling_context)
146
+ safe_dispatch_callback(
147
+ "traces_sampler",
148
+ configuration.traces_sampler,
149
+ [sampling_context],
150
+ fallback: configuration.traces_sample_rate
151
+ )
146
152
  elsif !sampling_context[:parent_sampled].nil?
147
153
  sampling_context[:parent_sampled]
148
154
  else
@@ -27,6 +27,11 @@ module Sentry
27
27
  backend.send_event(event)
28
28
  end
29
29
 
30
+ def send_envelope(envelope)
31
+ log_envelope(envelope)
32
+ backend.send_envelope(envelope)
33
+ end
34
+
30
35
  def log_envelope(envelope)
31
36
  envelope_json = {
32
37
  timestamp: Time.now.utc.iso8601,
@@ -36,13 +41,13 @@ module Sentry
36
41
  end
37
42
  }
38
43
 
39
- File.open(log_file, "a") { |file| file << JSON.dump(envelope_json) << "\n" }
44
+ File.open(log_file, "a") { |file| file.write("#{JSON.dump(envelope_json)}\n") }
40
45
  end
41
46
 
42
47
  def logged_envelopes
43
48
  return [] unless File.exist?(log_file)
44
49
 
45
- File.readlines(log_file).map do |line|
50
+ File.readlines(log_file).select { |line| fully_written?(line) }.map do |line|
46
51
  JSON.parse(line)
47
52
  end
48
53
  end
@@ -54,6 +59,10 @@ module Sentry
54
59
 
55
60
  private
56
61
 
62
+ def fully_written?(line)
63
+ line.end_with?("\n")
64
+ end
65
+
57
66
  def initialize_backend(configuration)
58
67
  backend = configuration.dsn.local? ? DummyTransport : HTTPTransport
59
68
  backend.new(configuration)