sentry-ruby 5.26.0 → 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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +27 -5
  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 +59 -136
  12. data/lib/sentry/configuration.rb +206 -78
  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/debug_structured_logger.rb +94 -0
  17. data/lib/sentry/dsn.rb +65 -1
  18. data/lib/sentry/envelope/item.rb +3 -3
  19. data/lib/sentry/error_event.rb +3 -3
  20. data/lib/sentry/event.rb +4 -10
  21. data/lib/sentry/exceptions.rb +3 -0
  22. data/lib/sentry/graphql.rb +1 -1
  23. data/lib/sentry/hub.rb +29 -5
  24. data/lib/sentry/interface.rb +1 -1
  25. data/lib/sentry/interfaces/exception.rb +2 -2
  26. data/lib/sentry/interfaces/request.rb +2 -0
  27. data/lib/sentry/interfaces/single_exception.rb +4 -4
  28. data/lib/sentry/interfaces/stacktrace.rb +3 -3
  29. data/lib/sentry/interfaces/stacktrace_builder.rb +0 -8
  30. data/lib/sentry/interfaces/threads.rb +2 -2
  31. data/lib/sentry/log_event.rb +33 -138
  32. data/lib/sentry/log_event_buffer.rb +13 -60
  33. data/lib/sentry/metric_event.rb +49 -0
  34. data/lib/sentry/metric_event_buffer.rb +28 -0
  35. data/lib/sentry/metrics.rb +47 -42
  36. data/lib/sentry/profiler.rb +4 -5
  37. data/lib/sentry/propagation_context.rb +101 -24
  38. data/lib/sentry/rack/capture_exceptions.rb +90 -2
  39. data/lib/sentry/release_detector.rb +1 -1
  40. data/lib/sentry/rspec.rb +1 -1
  41. data/lib/sentry/scope.rb +51 -18
  42. data/lib/sentry/sequel.rb +35 -0
  43. data/lib/sentry/span.rb +5 -17
  44. data/lib/sentry/std_lib_logger.rb +10 -1
  45. data/lib/sentry/telemetry_event_buffer.rb +130 -0
  46. data/lib/sentry/test_helper.rb +30 -0
  47. data/lib/sentry/transaction.rb +73 -95
  48. data/lib/sentry/transaction_event.rb +4 -9
  49. data/lib/sentry/transport/debug_transport.rb +70 -0
  50. data/lib/sentry/transport/dummy_transport.rb +1 -0
  51. data/lib/sentry/transport/http_transport.rb +16 -16
  52. data/lib/sentry/transport.rb +10 -7
  53. data/lib/sentry/utils/encoding_helper.rb +6 -0
  54. data/lib/sentry/utils/logging_helper.rb +25 -9
  55. data/lib/sentry/utils/sample_rand.rb +97 -0
  56. data/lib/sentry/utils/telemetry_attributes.rb +30 -0
  57. data/lib/sentry/vernier/profiler.rb +4 -3
  58. data/lib/sentry/version.rb +1 -1
  59. data/lib/sentry-ruby.rb +57 -30
  60. data/sentry-ruby-core.gemspec +1 -1
  61. data/sentry-ruby.gemspec +2 -1
  62. metadata +31 -17
  63. data/lib/sentry/metrics/aggregator.rb +0 -248
  64. data/lib/sentry/metrics/configuration.rb +0 -47
  65. data/lib/sentry/metrics/counter_metric.rb +0 -25
  66. data/lib/sentry/metrics/distribution_metric.rb +0 -25
  67. data/lib/sentry/metrics/gauge_metric.rb +0 -35
  68. data/lib/sentry/metrics/local_aggregator.rb +0 -53
  69. data/lib/sentry/metrics/metric.rb +0 -19
  70. data/lib/sentry/metrics/set_metric.rb +0 -28
  71. data/lib/sentry/metrics/timing.rb +0 -51
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/utils/telemetry_attributes"
4
+
5
+ module Sentry
6
+ class MetricEvent
7
+ include Sentry::Utils::TelemetryAttributes
8
+
9
+ attr_reader :name, :type, :value, :unit, :timestamp, :trace_id, :span_id, :attributes
10
+ attr_writer :trace_id, :span_id, :attributes
11
+
12
+ def initialize(
13
+ name:,
14
+ type:,
15
+ value:,
16
+ unit: nil,
17
+ attributes: nil
18
+ )
19
+ @name = name
20
+ @type = type
21
+ @value = value
22
+ @unit = unit
23
+ @attributes = attributes || {}
24
+
25
+ @timestamp = Sentry.utc_now
26
+ @trace_id = nil
27
+ @span_id = nil
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ name: @name,
33
+ type: @type,
34
+ value: @value,
35
+ unit: @unit,
36
+ timestamp: @timestamp.to_f,
37
+ trace_id: @trace_id,
38
+ span_id: @span_id,
39
+ attributes: serialize_attributes
40
+ }.compact
41
+ end
42
+
43
+ private
44
+
45
+ def serialize_attributes
46
+ @attributes.transform_values { |v| attribute_hash(v) }
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/telemetry_event_buffer"
4
+
5
+ module Sentry
6
+ # MetricEventBuffer buffers metric events and sends them to Sentry in a single envelope.
7
+ #
8
+ # This is used internally by the `Sentry::Client`.
9
+ #
10
+ # @!visibility private
11
+ class MetricEventBuffer < TelemetryEventBuffer
12
+ DEFAULT_MAX_METRICS = 1000
13
+ MAX_METRICS_BEFORE_DROP = 10_000
14
+
15
+ def initialize(configuration, client)
16
+ super(
17
+ configuration,
18
+ client,
19
+ event_class: MetricEvent,
20
+ max_items: configuration.max_metric_events || DEFAULT_MAX_METRICS,
21
+ max_items_before_drop: MAX_METRICS_BEFORE_DROP,
22
+ envelope_type: "trace_metric",
23
+ envelope_content_type: "application/vnd.sentry.items.trace-metric+json",
24
+ before_send: configuration.before_send_metric
25
+ )
26
+ end
27
+ end
28
+ end
@@ -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,91 @@ 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
+ # 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
+
94
+ def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
95
+ return unless baggage&.items
96
+
97
+ sample_rand_str = baggage.items["sample_rand"]
98
+ return unless sample_rand_str
99
+
100
+ generator = Utils::SampleRand.new(trace_id: trace_id)
101
+ generator.generate_from_value(sample_rand_str)
102
+ end
103
+
104
+ def self.generate_sample_rand(baggage, trace_id, parent_sampled)
105
+ generator = Utils::SampleRand.new(trace_id: trace_id)
106
+
107
+ if baggage&.items && !parent_sampled.nil?
108
+ sample_rate_str = baggage.items["sample_rate"]
109
+ sample_rate = sample_rate_str&.to_f
110
+
111
+ if sample_rate && !parent_sampled.nil?
112
+ generator.generate_from_sampling_decision(parent_sampled, sample_rate)
113
+ else
114
+ generator.generate_from_trace_id
115
+ end
116
+ else
117
+ generator.generate_from_trace_id
118
+ end
119
+ end
36
120
 
37
121
  def initialize(scope, env = nil)
38
122
  @scope = scope
@@ -40,6 +124,7 @@ module Sentry
40
124
  @parent_sampled = nil
41
125
  @baggage = nil
42
126
  @incoming_trace = false
127
+ @sample_rand = nil
43
128
 
44
129
  if env
45
130
  sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
@@ -49,9 +134,7 @@ module Sentry
49
134
  sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
50
135
 
51
136
  if sentry_trace_data
52
- @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
53
-
54
- @baggage =
137
+ incoming_baggage =
55
138
  if baggage_header && !baggage_header.empty?
56
139
  Baggage.from_incoming_header(baggage_header)
57
140
  else
@@ -61,28 +144,20 @@ module Sentry
61
144
  Baggage.new({})
62
145
  end
63
146
 
64
- @baggage.freeze!
65
- @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
66
154
  end
67
155
  end
68
156
  end
69
157
 
70
158
  @trace_id ||= Utils.uuid
71
159
  @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]
160
+ @sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
86
161
  end
87
162
 
88
163
  # Returns the trace context that can be used to embed in an Event.
@@ -123,9 +198,11 @@ module Sentry
123
198
 
124
199
  items = {
125
200
  "trace_id" => trace_id,
201
+ "sample_rand" => Utils::SampleRand.format(@sample_rand),
126
202
  "environment" => configuration.environment,
127
203
  "release" => configuration.release,
128
- "public_key" => configuration.dsn&.public_key
204
+ "public_key" => configuration.dsn&.public_key,
205
+ "org_id" => configuration.effective_org_id
129
206
  }
130
207
 
131
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