sentry-ruby 5.9.0 → 5.16.1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -10
  3. data/README.md +9 -9
  4. data/lib/sentry/background_worker.rb +8 -1
  5. data/lib/sentry/backpressure_monitor.rb +75 -0
  6. data/lib/sentry/breadcrumb.rb +8 -2
  7. data/lib/sentry/check_in_event.rb +60 -0
  8. data/lib/sentry/client.rb +48 -10
  9. data/lib/sentry/configuration.rb +89 -17
  10. data/lib/sentry/cron/configuration.rb +23 -0
  11. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  12. data/lib/sentry/cron/monitor_config.rb +53 -0
  13. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  14. data/lib/sentry/envelope.rb +1 -1
  15. data/lib/sentry/event.rb +6 -28
  16. data/lib/sentry/hub.rb +74 -2
  17. data/lib/sentry/integrable.rb +6 -0
  18. data/lib/sentry/interfaces/single_exception.rb +5 -3
  19. data/lib/sentry/net/http.rb +26 -20
  20. data/lib/sentry/profiler.rb +18 -7
  21. data/lib/sentry/propagation_context.rb +134 -0
  22. data/lib/sentry/puma.rb +11 -4
  23. data/lib/sentry/rack/capture_exceptions.rb +1 -4
  24. data/lib/sentry/rake.rb +0 -13
  25. data/lib/sentry/redis.rb +9 -3
  26. data/lib/sentry/release_detector.rb +1 -1
  27. data/lib/sentry/scope.rb +29 -13
  28. data/lib/sentry/span.rb +39 -2
  29. data/lib/sentry/test_helper.rb +18 -12
  30. data/lib/sentry/transaction.rb +18 -19
  31. data/lib/sentry/transaction_event.rb +0 -3
  32. data/lib/sentry/transport/configuration.rb +74 -1
  33. data/lib/sentry/transport/http_transport.rb +68 -37
  34. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  35. data/lib/sentry/transport.rb +21 -17
  36. data/lib/sentry/utils/argument_checking_helper.rb +9 -3
  37. data/lib/sentry/version.rb +1 -1
  38. data/lib/sentry-ruby.rb +83 -25
  39. metadata +10 -3
  40. data/CODE_OF_CONDUCT.md +0 -74
@@ -0,0 +1,75 @@
1
+ module Sentry
2
+ module Cron
3
+ module MonitorCheckIns
4
+ MAX_SLUG_LENGTH = 50
5
+
6
+ module Patch
7
+ def perform(*args, **opts)
8
+ slug = self.class.sentry_monitor_slug
9
+ monitor_config = self.class.sentry_monitor_config
10
+
11
+ check_in_id = Sentry.capture_check_in(slug,
12
+ :in_progress,
13
+ monitor_config: monitor_config)
14
+
15
+ start = Sentry.utc_now.to_i
16
+
17
+ begin
18
+ # need to do this on ruby <= 2.6 sadly
19
+ ret = method(:perform).super_method.arity == 0 ? super() : super
20
+ duration = Sentry.utc_now.to_i - start
21
+
22
+ Sentry.capture_check_in(slug,
23
+ :ok,
24
+ check_in_id: check_in_id,
25
+ duration: duration,
26
+ monitor_config: monitor_config)
27
+
28
+ ret
29
+ rescue Exception
30
+ duration = Sentry.utc_now.to_i - start
31
+
32
+ Sentry.capture_check_in(slug,
33
+ :error,
34
+ check_in_id: check_in_id,
35
+ duration: duration,
36
+ monitor_config: monitor_config)
37
+
38
+ raise
39
+ end
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+ def sentry_monitor_check_ins(slug: nil, monitor_config: nil)
45
+ if monitor_config && Sentry.configuration
46
+ cron_config = Sentry.configuration.cron
47
+ monitor_config.checkin_margin ||= cron_config.default_checkin_margin
48
+ monitor_config.max_runtime ||= cron_config.default_max_runtime
49
+ monitor_config.timezone ||= cron_config.default_timezone
50
+ end
51
+
52
+ @sentry_monitor_slug = slug
53
+ @sentry_monitor_config = monitor_config
54
+
55
+ prepend Patch
56
+ end
57
+
58
+ def sentry_monitor_slug(name: self.name)
59
+ @sentry_monitor_slug ||= begin
60
+ slug = name.gsub('::', '-').downcase
61
+ slug[-MAX_SLUG_LENGTH..-1] || slug
62
+ end
63
+ end
64
+
65
+ def sentry_monitor_config
66
+ @sentry_monitor_config
67
+ end
68
+ end
69
+
70
+ def self.included(base)
71
+ base.extend(ClassMethods)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sentry/cron/monitor_schedule'
4
+
5
+ module Sentry
6
+ module Cron
7
+ class MonitorConfig
8
+ # The monitor schedule configuration
9
+ # @return [MonitorSchedule::Crontab, MonitorSchedule::Interval]
10
+ attr_accessor :schedule
11
+
12
+ # How long (in minutes) after the expected checkin time will we wait
13
+ # until we consider the checkin to have been missed.
14
+ # @return [Integer, nil]
15
+ attr_accessor :checkin_margin
16
+
17
+ # How long (in minutes) is the checkin allowed to run for in in_progress
18
+ # before it is considered failed.
19
+ # @return [Integer, nil]
20
+ attr_accessor :max_runtime
21
+
22
+ # tz database style timezone string
23
+ # @return [String, nil]
24
+ attr_accessor :timezone
25
+
26
+ def initialize(schedule, checkin_margin: nil, max_runtime: nil, timezone: nil)
27
+ @schedule = schedule
28
+ @checkin_margin = checkin_margin
29
+ @max_runtime = max_runtime
30
+ @timezone = timezone
31
+ end
32
+
33
+ def self.from_crontab(crontab, **options)
34
+ new(MonitorSchedule::Crontab.new(crontab), **options)
35
+ end
36
+
37
+ def self.from_interval(num, unit, **options)
38
+ return nil unless MonitorSchedule::Interval::VALID_UNITS.include?(unit)
39
+
40
+ new(MonitorSchedule::Interval.new(num, unit), **options)
41
+ end
42
+
43
+ def to_hash
44
+ {
45
+ schedule: schedule.to_hash,
46
+ checkin_margin: checkin_margin,
47
+ max_runtime: max_runtime,
48
+ timezone: timezone
49
+ }.compact
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Cron
5
+ module MonitorSchedule
6
+ class Crontab
7
+ # A crontab formatted string such as "0 * * * *".
8
+ # @return [String]
9
+ attr_accessor :value
10
+
11
+ def initialize(value)
12
+ @value = value
13
+ end
14
+
15
+ def to_hash
16
+ { type: :crontab, value: value }
17
+ end
18
+ end
19
+
20
+ class Interval
21
+ # The number representing duration of the interval.
22
+ # @return [Integer]
23
+ attr_accessor :value
24
+
25
+ # The unit representing duration of the interval.
26
+ # @return [Symbol]
27
+ attr_accessor :unit
28
+
29
+ VALID_UNITS = %i(year month week day hour minute)
30
+
31
+ def initialize(value, unit)
32
+ @value = value
33
+ @unit = unit
34
+ end
35
+
36
+ def to_hash
37
+ { type: :interval, value: value, unit: unit }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -5,7 +5,7 @@ module Sentry
5
5
  class Envelope
6
6
  class Item
7
7
  STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
8
- MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
8
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
9
9
 
10
10
  attr_accessor :headers, :payload
11
11
 
data/lib/sentry/event.rb CHANGED
@@ -37,6 +37,11 @@ module Sentry
37
37
  # @return [RequestInterface]
38
38
  attr_reader :request
39
39
 
40
+ # Dynamic Sampling Context (DSC) that gets attached
41
+ # as the trace envelope header in the transport.
42
+ # @return [Hash, nil]
43
+ attr_accessor :dynamic_sampling_context
44
+
40
45
  # @param configuration [Configuration]
41
46
  # @param integration_meta [Hash, nil]
42
47
  # @param message [String, nil]
@@ -54,6 +59,7 @@ module Sentry
54
59
  @tags = {}
55
60
 
56
61
  @fingerprint = []
62
+ @dynamic_sampling_context = nil
57
63
 
58
64
  # configuration data that's directly used by events
59
65
  @server_name = configuration.server_name
@@ -70,34 +76,6 @@ module Sentry
70
76
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
71
77
  end
72
78
 
73
- class << self
74
- # @!visibility private
75
- def get_log_message(event_hash)
76
- message = event_hash[:message] || event_hash['message']
77
-
78
- return message unless message.nil? || message.empty?
79
-
80
- message = get_message_from_exception(event_hash)
81
-
82
- return message unless message.nil? || message.empty?
83
-
84
- message = event_hash[:transaction] || event_hash["transaction"]
85
-
86
- return message unless message.nil? || message.empty?
87
-
88
- '<no message value>'
89
- end
90
-
91
- # @!visibility private
92
- def get_message_from_exception(event_hash)
93
- if exception = event_hash.dig(:exception, :values, 0)
94
- "#{exception[:type]}: #{exception[:value]}"
95
- elsif exception = event_hash.dig("exception", "values", 0)
96
- "#{exception["type"]}: #{exception["value"]}"
97
- end
98
- end
99
- end
100
-
101
79
  # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
102
80
  # @return [Configuration]
103
81
  def configuration
data/lib/sentry/hub.rb CHANGED
@@ -116,7 +116,11 @@ module Sentry
116
116
  end
117
117
 
118
118
  def capture_exception(exception, **options, &block)
119
- check_argument_type!(exception, ::Exception)
119
+ if RUBY_PLATFORM == "java"
120
+ check_argument_type!(exception, ::Exception, ::Java::JavaLang::Throwable)
121
+ else
122
+ check_argument_type!(exception, ::Exception)
123
+ end
120
124
 
121
125
  return if Sentry.exception_captured?(exception)
122
126
 
@@ -152,6 +156,30 @@ module Sentry
152
156
  capture_event(event, **options, &block)
153
157
  end
154
158
 
159
+ def capture_check_in(slug, status, **options)
160
+ check_argument_type!(slug, ::String)
161
+ check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES)
162
+
163
+ return unless current_client
164
+
165
+ options[:hint] ||= {}
166
+ options[:hint][:slug] = slug
167
+
168
+ event = current_client.event_from_check_in(
169
+ slug,
170
+ status,
171
+ options[:hint],
172
+ duration: options.delete(:duration),
173
+ monitor_config: options.delete(:monitor_config),
174
+ check_in_id: options.delete(:check_in_id)
175
+ )
176
+
177
+ return unless event
178
+
179
+ capture_event(event, **options)
180
+ event.check_in_id
181
+ end
182
+
155
183
  def capture_event(event, **options, &block)
156
184
  check_argument_type!(event, Sentry::Event)
157
185
 
@@ -174,7 +202,7 @@ module Sentry
174
202
  configuration.log_debug(event.to_json_compatible)
175
203
  end
176
204
 
177
- @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
205
+ @last_event_id = event&.event_id if event.is_a?(Sentry::ErrorEvent)
178
206
  event
179
207
  end
180
208
 
@@ -225,6 +253,50 @@ module Sentry
225
253
  end_session
226
254
  end
227
255
 
256
+ def get_traceparent
257
+ return nil unless current_scope
258
+
259
+ current_scope.get_span&.to_sentry_trace ||
260
+ current_scope.propagation_context.get_traceparent
261
+ end
262
+
263
+ def get_baggage
264
+ return nil unless current_scope
265
+
266
+ current_scope.get_span&.to_baggage ||
267
+ current_scope.propagation_context.get_baggage&.serialize
268
+ end
269
+
270
+ def get_trace_propagation_headers
271
+ headers = {}
272
+
273
+ traceparent = get_traceparent
274
+ headers[SENTRY_TRACE_HEADER_NAME] = traceparent if traceparent
275
+
276
+ baggage = get_baggage
277
+ headers[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
278
+
279
+ headers
280
+ end
281
+
282
+ def continue_trace(env, **options)
283
+ configure_scope { |s| s.generate_propagation_context(env) }
284
+
285
+ return nil unless configuration.tracing_enabled?
286
+
287
+ propagation_context = current_scope.propagation_context
288
+ return nil unless propagation_context.incoming_trace
289
+
290
+ Transaction.new(
291
+ hub: self,
292
+ trace_id: propagation_context.trace_id,
293
+ parent_span_id: propagation_context.parent_span_id,
294
+ parent_sampled: propagation_context.parent_sampled,
295
+ baggage: propagation_context.baggage,
296
+ **options
297
+ )
298
+ end
299
+
228
300
  private
229
301
 
230
302
  def current_layer
@@ -22,5 +22,11 @@ module Sentry
22
22
  options[:hint][:integration] = integration_name
23
23
  Sentry.capture_message(message, **options, &block)
24
24
  end
25
+
26
+ def capture_check_in(slug, status, **options, &block)
27
+ options[:hint] ||= {}
28
+ options[:hint][:integration] = integration_name
29
+ Sentry.capture_check_in(slug, status, **options, &block)
30
+ end
25
31
  end
26
32
  end
@@ -11,7 +11,8 @@ module Sentry
11
11
  OMISSION_MARK = "...".freeze
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
- attr_reader :type, :value, :module, :thread_id, :stacktrace
14
+ attr_reader :type, :module, :thread_id, :stacktrace
15
+ attr_accessor :value
15
16
 
16
17
  def initialize(exception:, stacktrace: nil)
17
18
  @type = exception.class.to_s
@@ -21,8 +22,9 @@ module Sentry
21
22
  else
22
23
  exception.message || ""
23
24
  end
25
+ exception_message = exception_message.inspect unless exception_message.is_a?(String)
24
26
 
25
- @value = exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
27
+ @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
26
28
 
27
29
  @module = exception.class.to_s.split('::')[0...-1].join('::')
28
30
  @thread_id = Thread.current.object_id
@@ -50,7 +52,7 @@ module Sentry
50
52
  v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
51
53
  end
52
54
 
53
- v
55
+ Utils::EncodingHelper.encode_to_utf_8(v)
54
56
  rescue StandardError
55
57
  PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
56
58
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
+ require "resolv"
4
5
 
5
6
  module Sentry
6
7
  # @api private
@@ -30,15 +31,21 @@ module Sentry
30
31
  return super if from_sentry_sdk?
31
32
 
32
33
  Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
33
- set_sentry_trace_header(req, sentry_span)
34
+ request_info = extract_request_info(req)
35
+
36
+ if propagate_trace?(request_info[:url], Sentry.configuration)
37
+ set_propagation_headers(req)
38
+ end
34
39
 
35
40
  super.tap do |res|
36
- record_sentry_breadcrumb(req, res)
41
+ record_sentry_breadcrumb(request_info, res)
37
42
 
38
43
  if sentry_span
39
- request_info = extract_request_info(req)
40
44
  sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
41
- sentry_span.set_data(:status, res.code.to_i)
45
+ sentry_span.set_data(Span::DataConventions::URL, request_info[:url])
46
+ sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method])
47
+ sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query]
48
+ sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, res.code.to_i)
42
49
  end
43
50
  end
44
51
  end
@@ -46,23 +53,13 @@ module Sentry
46
53
 
47
54
  private
48
55
 
49
- def set_sentry_trace_header(req, sentry_span)
50
- return unless sentry_span
51
-
52
- client = Sentry.get_current_client
53
-
54
- trace = client.generate_sentry_trace(sentry_span)
55
- req[SENTRY_TRACE_HEADER_NAME] = trace if trace
56
-
57
- baggage = client.generate_baggage(sentry_span)
58
- req[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
56
+ def set_propagation_headers(req)
57
+ Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
59
58
  end
60
59
 
61
- def record_sentry_breadcrumb(req, res)
60
+ def record_sentry_breadcrumb(request_info, res)
62
61
  return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
63
62
 
64
- request_info = extract_request_info(req)
65
-
66
63
  crumb = Sentry::Breadcrumb.new(
67
64
  level: :info,
68
65
  category: BREADCRUMB_CATEGORY,
@@ -81,20 +78,29 @@ module Sentry
81
78
  end
82
79
 
83
80
  def extract_request_info(req)
84
- uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
81
+ # IPv6 url could look like '::1/path', and that won't parse without
82
+ # wrapping it in square brackets.
83
+ hostname = address =~ Resolv::IPv6::Regex ? "[#{address}]" : address
84
+ uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}")
85
85
  url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
86
86
 
87
87
  result = { method: req.method, url: url }
88
88
 
89
89
  if Sentry.configuration.send_default_pii
90
- result[:url] = result[:url] + "?#{uri.query}"
90
+ result[:query] = uri.query
91
91
  result[:body] = req.body
92
92
  end
93
93
 
94
94
  result
95
95
  end
96
+
97
+ def propagate_trace?(url, configuration)
98
+ url &&
99
+ configuration.propagate_traces &&
100
+ configuration.trace_propagation_targets.any? { |target| url.match?(target) }
101
+ end
96
102
  end
97
103
  end
98
104
  end
99
105
 
100
- Sentry.register_patch(Sentry::Net::HTTP, Net::HTTP)
106
+ Sentry.register_patch(:http, Sentry::Net::HTTP, Net::HTTP)
@@ -9,6 +9,7 @@ module Sentry
9
9
  # 101 Hz in microseconds
10
10
  DEFAULT_INTERVAL = 1e6 / 101
11
11
  MICRO_TO_NANO_SECONDS = 1e3
12
+ MIN_SAMPLES_REQUIRED = 2
12
13
 
13
14
  attr_reader :sampled, :started, :event_id
14
15
 
@@ -73,14 +74,19 @@ module Sentry
73
74
  end
74
75
 
75
76
  def to_hash
76
- return {} unless @sampled
77
+ unless @sampled
78
+ record_lost_event(:sample_rate)
79
+ return {}
80
+ end
81
+
77
82
  return {} unless @started
78
83
 
79
84
  results = StackProf.results
80
- return {} unless results
81
- return {} if results.empty?
82
- return {} if results[:samples] == 0
83
- return {} unless results[:raw]
85
+
86
+ if !results || results.empty? || results[:samples] == 0 || !results[:raw]
87
+ record_lost_event(:insufficient_data)
88
+ return {}
89
+ end
84
90
 
85
91
  frame_map = {}
86
92
 
@@ -103,7 +109,7 @@ module Sentry
103
109
  }
104
110
 
105
111
  frame_hash[:module] = mod if mod
106
- frame_hash[:lineno] = frame_data[:line] if frame_data[:line]
112
+ frame_hash[:lineno] = frame_data[:line] if frame_data[:line] && frame_data[:line] >= 0
107
113
 
108
114
  frame_hash
109
115
  end
@@ -157,8 +163,9 @@ module Sentry
157
163
 
158
164
  log('Some samples thrown away') if samples.size != results[:samples]
159
165
 
160
- if samples.size <= 2
166
+ if samples.size <= MIN_SAMPLES_REQUIRED
161
167
  log('Not enough samples, discarding profiler')
168
+ record_lost_event(:insufficient_data)
162
169
  return {}
163
170
  end
164
171
 
@@ -218,5 +225,9 @@ module Sentry
218
225
 
219
226
  [function, mod]
220
227
  end
228
+
229
+ def record_lost_event(reason)
230
+ Sentry.get_current_client&.transport&.record_lost_event(reason, 'profile')
231
+ end
221
232
  end
222
233
  end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "sentry/baggage"
5
+
6
+ module Sentry
7
+ class PropagationContext
8
+ SENTRY_TRACE_REGEXP = Regexp.new(
9
+ "^[ \t]*" + # whitespace
10
+ "([0-9a-f]{32})?" + # trace_id
11
+ "-?([0-9a-f]{16})?" + # span_id
12
+ "-?([01])?" + # sampled
13
+ "[ \t]*$" # whitespace
14
+ )
15
+
16
+ # An uuid that can be used to identify a trace.
17
+ # @return [String]
18
+ attr_reader :trace_id
19
+ # An uuid that can be used to identify the span.
20
+ # @return [String]
21
+ attr_reader :span_id
22
+ # Span parent's span_id.
23
+ # @return [String, nil]
24
+ attr_reader :parent_span_id
25
+ # The sampling decision of the parent transaction.
26
+ # @return [Boolean, nil]
27
+ attr_reader :parent_sampled
28
+ # Is there an incoming trace or not?
29
+ # @return [Boolean]
30
+ attr_reader :incoming_trace
31
+ # This is only for accessing the current baggage variable.
32
+ # Please use the #get_baggage method for interfacing outside this class.
33
+ # @return [Baggage, nil]
34
+ attr_reader :baggage
35
+
36
+ def initialize(scope, env = nil)
37
+ @scope = scope
38
+ @parent_span_id = nil
39
+ @parent_sampled = nil
40
+ @baggage = nil
41
+ @incoming_trace = false
42
+
43
+ if env
44
+ sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
45
+ baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]
46
+
47
+ if sentry_trace_header
48
+ sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
49
+
50
+ if sentry_trace_data
51
+ @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
52
+
53
+ @baggage = if baggage_header && !baggage_header.empty?
54
+ Baggage.from_incoming_header(baggage_header)
55
+ else
56
+ # If there's an incoming sentry-trace but no incoming baggage header,
57
+ # for instance in traces coming from older SDKs,
58
+ # baggage will be empty and frozen and won't be populated as head SDK.
59
+ Baggage.new({})
60
+ end
61
+
62
+ @baggage.freeze!
63
+ @incoming_trace = true
64
+ end
65
+ end
66
+ end
67
+
68
+ @trace_id ||= SecureRandom.uuid.delete("-")
69
+ @span_id = SecureRandom.uuid.delete("-").slice(0, 16)
70
+ end
71
+
72
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
73
+ #
74
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
75
+ # @return [Array, nil]
76
+ def self.extract_sentry_trace(sentry_trace)
77
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
78
+ return nil if match.nil?
79
+
80
+ trace_id, parent_span_id, sampled_flag = match[1..3]
81
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
82
+
83
+ [trace_id, parent_span_id, parent_sampled]
84
+ end
85
+
86
+ # Returns the trace context that can be used to embed in an Event.
87
+ # @return [Hash]
88
+ def get_trace_context
89
+ {
90
+ trace_id: trace_id,
91
+ span_id: span_id,
92
+ parent_span_id: parent_span_id
93
+ }
94
+ end
95
+
96
+ # Returns the sentry-trace header from the propagation context.
97
+ # @return [String]
98
+ def get_traceparent
99
+ "#{trace_id}-#{span_id}"
100
+ end
101
+
102
+ # Returns the Baggage from the propagation context or populates as head SDK if empty.
103
+ # @return [Baggage, nil]
104
+ def get_baggage
105
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
106
+ @baggage
107
+ end
108
+
109
+ # Returns the Dynamic Sampling Context from the baggage.
110
+ # @return [String, nil]
111
+ def get_dynamic_sampling_context
112
+ get_baggage&.dynamic_sampling_context
113
+ end
114
+
115
+ private
116
+
117
+ def populate_head_baggage
118
+ return unless Sentry.initialized?
119
+
120
+ configuration = Sentry.configuration
121
+
122
+ items = {
123
+ "trace_id" => trace_id,
124
+ "environment" => configuration.environment,
125
+ "release" => configuration.release,
126
+ "public_key" => configuration.dsn&.public_key,
127
+ "user_segment" => @scope.user && @scope.user["segment"]
128
+ }
129
+
130
+ items.compact!
131
+ @baggage = Baggage.new(items, mutable: false)
132
+ end
133
+ end
134
+ end