sentry-ruby 5.26.0 → 6.0.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -4
  3. data/lib/sentry/background_worker.rb +1 -4
  4. data/lib/sentry/breadcrumb.rb +1 -1
  5. data/lib/sentry/breadcrumb_buffer.rb +2 -2
  6. data/lib/sentry/check_in_event.rb +2 -2
  7. data/lib/sentry/client.rb +29 -89
  8. data/lib/sentry/configuration.rb +125 -78
  9. data/lib/sentry/cron/monitor_check_ins.rb +3 -3
  10. data/lib/sentry/cron/monitor_config.rb +2 -2
  11. data/lib/sentry/cron/monitor_schedule.rb +2 -2
  12. data/lib/sentry/debug_structured_logger.rb +94 -0
  13. data/lib/sentry/dsn.rb +32 -0
  14. data/lib/sentry/envelope/item.rb +1 -2
  15. data/lib/sentry/error_event.rb +3 -3
  16. data/lib/sentry/event.rb +4 -10
  17. data/lib/sentry/graphql.rb +1 -1
  18. data/lib/sentry/hub.rb +6 -5
  19. data/lib/sentry/interface.rb +1 -1
  20. data/lib/sentry/interfaces/exception.rb +2 -2
  21. data/lib/sentry/interfaces/request.rb +2 -0
  22. data/lib/sentry/interfaces/single_exception.rb +3 -3
  23. data/lib/sentry/interfaces/stacktrace.rb +3 -3
  24. data/lib/sentry/interfaces/stacktrace_builder.rb +0 -8
  25. data/lib/sentry/interfaces/threads.rb +2 -2
  26. data/lib/sentry/log_event.rb +19 -6
  27. data/lib/sentry/profiler.rb +4 -5
  28. data/lib/sentry/propagation_context.rb +55 -18
  29. data/lib/sentry/rspec.rb +1 -1
  30. data/lib/sentry/span.rb +2 -17
  31. data/lib/sentry/std_lib_logger.rb +6 -1
  32. data/lib/sentry/test_helper.rb +23 -0
  33. data/lib/sentry/transaction.rb +72 -95
  34. data/lib/sentry/transaction_event.rb +4 -9
  35. data/lib/sentry/transport/debug_transport.rb +70 -0
  36. data/lib/sentry/transport/dummy_transport.rb +1 -0
  37. data/lib/sentry/transport/http_transport.rb +9 -5
  38. data/lib/sentry/transport.rb +3 -5
  39. data/lib/sentry/utils/logging_helper.rb +8 -6
  40. data/lib/sentry/utils/sample_rand.rb +97 -0
  41. data/lib/sentry/vernier/profiler.rb +4 -3
  42. data/lib/sentry/version.rb +1 -1
  43. data/lib/sentry-ruby.rb +6 -30
  44. data/sentry-ruby-core.gemspec +1 -1
  45. data/sentry-ruby.gemspec +1 -1
  46. metadata +11 -18
  47. data/lib/sentry/metrics/aggregator.rb +0 -248
  48. data/lib/sentry/metrics/configuration.rb +0 -47
  49. data/lib/sentry/metrics/counter_metric.rb +0 -25
  50. data/lib/sentry/metrics/distribution_metric.rb +0 -25
  51. data/lib/sentry/metrics/gauge_metric.rb +0 -35
  52. data/lib/sentry/metrics/local_aggregator.rb +0 -53
  53. data/lib/sentry/metrics/metric.rb +0 -19
  54. data/lib/sentry/metrics/set_metric.rb +0 -28
  55. data/lib/sentry/metrics/timing.rb +0 -51
  56. data/lib/sentry/metrics.rb +0 -56
@@ -14,12 +14,12 @@ module Sentry
14
14
  :in_progress,
15
15
  monitor_config: monitor_config)
16
16
 
17
- start = Metrics::Timing.duration_start
17
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
18
18
 
19
19
  begin
20
20
  # need to do this on ruby <= 2.6 sadly
21
21
  ret = method(:perform).super_method.arity == 0 ? super() : super
22
- duration = Metrics::Timing.duration_end(start)
22
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
23
23
 
24
24
  Sentry.capture_check_in(slug,
25
25
  :ok,
@@ -29,7 +29,7 @@ module Sentry
29
29
 
30
30
  ret
31
31
  rescue Exception
32
- duration = Metrics::Timing.duration_end(start)
32
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
33
33
 
34
34
  Sentry.capture_check_in(slug,
35
35
  :error,
@@ -40,9 +40,9 @@ module Sentry
40
40
  new(MonitorSchedule::Interval.new(num, unit), **options)
41
41
  end
42
42
 
43
- def to_hash
43
+ def to_h
44
44
  {
45
- schedule: schedule.to_hash,
45
+ schedule: schedule.to_h,
46
46
  checkin_margin: checkin_margin,
47
47
  max_runtime: max_runtime,
48
48
  timezone: timezone
@@ -12,7 +12,7 @@ module Sentry
12
12
  @value = value
13
13
  end
14
14
 
15
- def to_hash
15
+ def to_h
16
16
  { type: :crontab, value: value }
17
17
  end
18
18
  end
@@ -33,7 +33,7 @@ module Sentry
33
33
  @unit = unit
34
34
  end
35
35
 
36
- def to_hash
36
+ def to_h
37
37
  { type: :interval, value: value, unit: unit }
38
38
  end
39
39
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "pathname"
6
+ require "delegate"
7
+
8
+ module Sentry
9
+ # DebugStructuredLogger is a logger that captures structured log events to a file for debugging purposes.
10
+ #
11
+ # It can optionally also send log events to Sentry via the normal structured logger if logging
12
+ # is enabled.
13
+ class DebugStructuredLogger < SimpleDelegator
14
+ DEFAULT_LOG_FILE_PATH = File.join("log", "sentry_debug_logs.log")
15
+
16
+ attr_reader :log_file, :backend
17
+
18
+ def initialize(configuration)
19
+ @log_file = initialize_log_file(
20
+ configuration.structured_logging.file_path || DEFAULT_LOG_FILE_PATH
21
+ )
22
+ @backend = initialize_backend(configuration)
23
+
24
+ super(@backend)
25
+ end
26
+
27
+ # Override all log level methods to capture events
28
+ %i[trace debug info warn error fatal].each do |level|
29
+ define_method(level) do |message, parameters = [], **attributes|
30
+ log_event = capture_log_event(level, message, parameters, **attributes)
31
+ backend.public_send(level, message, parameters, **attributes)
32
+ log_event
33
+ end
34
+ end
35
+
36
+ def log(level, message, parameters:, **attributes)
37
+ log_event = capture_log_event(level, message, parameters, **attributes)
38
+ backend.log(level, message, parameters: parameters, **attributes)
39
+ log_event
40
+ end
41
+
42
+ def capture_log_event(level, message, parameters, **attributes)
43
+ log_event_json = {
44
+ timestamp: Time.now.utc.iso8601,
45
+ level: level.to_s,
46
+ message: message,
47
+ parameters: parameters,
48
+ attributes: attributes
49
+ }
50
+
51
+ File.open(log_file, "a") { |file| file << JSON.dump(log_event_json) << "\n" }
52
+ log_event_json
53
+ end
54
+
55
+ def logged_events
56
+ File.readlines(log_file).map do |line|
57
+ JSON.parse(line)
58
+ end
59
+ end
60
+
61
+ def clear
62
+ File.write(log_file, "")
63
+ if backend.respond_to?(:config)
64
+ backend.config.sdk_logger.debug("DebugStructuredLogger: Cleared events from #{log_file}")
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def initialize_backend(configuration)
71
+ if configuration.enable_logs
72
+ StructuredLogger.new(configuration)
73
+ else
74
+ # Create a no-op logger if logging is disabled
75
+ NoOpLogger.new
76
+ end
77
+ end
78
+
79
+ def initialize_log_file(log_file_path)
80
+ log_file = Pathname(log_file_path)
81
+
82
+ FileUtils.mkdir_p(log_file.dirname) unless log_file.dirname.exist?
83
+
84
+ log_file
85
+ end
86
+
87
+ # No-op logger for when structured logging is disabled
88
+ class NoOpLogger
89
+ %i[trace debug info warn error fatal log].each do |method|
90
+ define_method(method) { |*args, **kwargs| nil }
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/sentry/dsn.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "uri"
4
+ require "ipaddr"
5
+ require "resolv"
4
6
 
5
7
  module Sentry
6
8
  class DSN
7
9
  PORT_MAP = { "http" => 80, "https" => 443 }.freeze
8
10
  REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze
11
+ LOCALHOST_NAMES = %w[localhost 127.0.0.1 ::1 [::1]].freeze
12
+ LOCALHOST_PATTERN = /\.local(host|domain)?$/i
9
13
 
10
14
  attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
11
15
 
@@ -49,5 +53,33 @@ module Sentry
49
53
  def envelope_endpoint
50
54
  "#{path}/api/#{project_id}/envelope/"
51
55
  end
56
+
57
+ def local?
58
+ @local ||= (localhost? || private_ip? || resolved_ips_private?)
59
+ end
60
+
61
+ def localhost?
62
+ LOCALHOST_NAMES.include?(host.downcase) || LOCALHOST_PATTERN.match?(host)
63
+ end
64
+
65
+ def private_ip?
66
+ @private_ip ||= begin
67
+ begin
68
+ IPAddr.new(host).private?
69
+ rescue IPAddr::InvalidAddressError
70
+ false
71
+ end
72
+ end
73
+ end
74
+
75
+ def resolved_ips_private?
76
+ @resolved_ips_private ||= begin
77
+ begin
78
+ Resolv.getaddresses(host).any? { |ip| IPAddr.new(ip).private? }
79
+ rescue Resolv::ResolvError, IPAddr::InvalidAddressError
80
+ false
81
+ end
82
+ end
83
+ end
52
84
  end
53
85
  end
@@ -3,7 +3,7 @@
3
3
  module Sentry
4
4
  # @api private
5
5
  class Envelope::Item
6
- STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
6
+ STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 1000
7
7
  MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000
8
8
 
9
9
  SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update(
@@ -18,7 +18,6 @@ module Sentry
18
18
  when "session", "attachment", "transaction", "profile", "span", "log" then type
19
19
  when "sessions" then "session"
20
20
  when "check_in" then "monitor"
21
- when "statsd", "metric_meta" then "metric_bucket"
22
21
  when "event" then "error"
23
22
  when "client_report" then "internal"
24
23
  else "default"
@@ -10,10 +10,10 @@ module Sentry
10
10
  attr_reader :threads
11
11
 
12
12
  # @return [Hash]
13
- def to_hash
13
+ def to_h
14
14
  data = super
15
- data[:threads] = threads.to_hash if threads
16
- data[:exception] = exception.to_hash if exception
15
+ data[:threads] = threads.to_h if threads
16
+ data[:exception] = exception.to_h if exception
17
17
  data
18
18
  end
19
19
 
data/lib/sentry/event.rb CHANGED
@@ -81,12 +81,6 @@ module Sentry
81
81
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
82
82
  end
83
83
 
84
- # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
85
- # @return [Configuration]
86
- def configuration
87
- Sentry.configuration
88
- end
89
-
90
84
  # Sets the event's timestamp.
91
85
  # @param time [Time, Float]
92
86
  # @return [void]
@@ -118,16 +112,16 @@ module Sentry
118
112
  end
119
113
 
120
114
  # @return [Hash]
121
- def to_hash
115
+ def to_h
122
116
  data = serialize_attributes
123
- data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
124
- data[:request] = request.to_hash if request
117
+ data[:breadcrumbs] = breadcrumbs.to_h if breadcrumbs
118
+ data[:request] = request.to_h if request
125
119
  data
126
120
  end
127
121
 
128
122
  # @return [Hash]
129
123
  def to_json_compatible
130
- JSON.parse(JSON.generate(to_hash))
124
+ JSON.parse(JSON.generate(to_h))
131
125
  end
132
126
 
133
127
  private
@@ -4,6 +4,6 @@ Sentry.register_patch(:graphql) do |config|
4
4
  if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with)
5
5
  ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true)
6
6
  else
7
- config.logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile." }
7
+ config.sdk_logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile." }
8
8
  end
9
9
  end
data/lib/sentry/hub.rb CHANGED
@@ -116,11 +116,12 @@ module Sentry
116
116
  return unless configuration.tracing_enabled?
117
117
  return unless instrumenter == configuration.instrumenter
118
118
 
119
- transaction ||= Transaction.new(**options.merge(hub: self))
119
+ transaction ||= Transaction.new(**options)
120
120
 
121
121
  sampling_context = {
122
- transaction_context: transaction.to_hash,
123
- parent_sampled: transaction.parent_sampled
122
+ transaction_context: transaction.to_h,
123
+ parent_sampled: transaction.parent_sampled,
124
+ parent_sample_rate: transaction.parent_sample_rate
124
125
  }
125
126
 
126
127
  sampling_context.merge!(custom_sampling_context)
@@ -217,7 +218,7 @@ module Sentry
217
218
  end
218
219
 
219
220
  def capture_log_event(message, **options)
220
- return unless current_client
221
+ return unless current_client && current_client.configuration.enable_logs
221
222
 
222
223
  event = current_client.event_from_log(message, **options)
223
224
 
@@ -352,11 +353,11 @@ module Sentry
352
353
  return nil unless propagation_context.incoming_trace
353
354
 
354
355
  Transaction.new(
355
- hub: self,
356
356
  trace_id: propagation_context.trace_id,
357
357
  parent_span_id: propagation_context.parent_span_id,
358
358
  parent_sampled: propagation_context.parent_sampled,
359
359
  baggage: propagation_context.baggage,
360
+ sample_rand: propagation_context.sample_rand,
360
361
  **options
361
362
  )
362
363
  end
@@ -3,7 +3,7 @@
3
3
  module Sentry
4
4
  class Interface
5
5
  # @return [Hash]
6
- def to_hash
6
+ def to_h
7
7
  Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }]
8
8
  end
9
9
  end
@@ -13,9 +13,9 @@ module Sentry
13
13
  end
14
14
 
15
15
  # @return [Hash]
16
- def to_hash
16
+ def to_h
17
17
  data = super
18
- data[:values] = data[:values].map(&:to_hash) if data[:values]
18
+ data[:values] = data[:values].map(&:to_h) if data[:values]
19
19
  data
20
20
  end
21
21
 
@@ -69,6 +69,8 @@ module Sentry
69
69
  private
70
70
 
71
71
  def read_data_from(request)
72
+ return "Skipped non-rewindable request body" unless request.body.respond_to?(:rewind)
73
+
72
74
  if request.form_data?
73
75
  request.POST
74
76
  elsif request.body # JSON requests, etc
@@ -32,10 +32,10 @@ module Sentry
32
32
  @mechanism = mechanism
33
33
  end
34
34
 
35
- def to_hash
35
+ def to_h
36
36
  data = super
37
- data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
38
- data[:mechanism] = data[:mechanism].to_hash
37
+ data[:stacktrace] = data[:stacktrace].to_h if data[:stacktrace]
38
+ data[:mechanism] = data[:mechanism].to_h
39
39
  data
40
40
  end
41
41
 
@@ -11,8 +11,8 @@ module Sentry
11
11
  end
12
12
 
13
13
  # @return [Hash]
14
- def to_hash
15
- { frames: @frames.map(&:to_hash) }
14
+ def to_h
15
+ { frames: @frames.map(&:to_h) }
16
16
  end
17
17
 
18
18
  # @return [String]
@@ -66,7 +66,7 @@ module Sentry
66
66
  linecache.get_file_context(abs_path, lineno, context_lines)
67
67
  end
68
68
 
69
- def to_hash(*args)
69
+ def to_h(*args)
70
70
  data = super(*args)
71
71
  data.delete(:vars) unless vars && !vars.empty?
72
72
  data.delete(:pre_context) unless pre_context && !pre_context.empty?
@@ -75,14 +75,6 @@ module Sentry
75
75
  StacktraceInterface.new(frames: frames)
76
76
  end
77
77
 
78
- # Get the code location hash for a single line for where metrics where added.
79
- # @return [Hash]
80
- def metrics_code_location(unparsed_line)
81
- parsed_line = Backtrace::Line.parse(unparsed_line)
82
- frame = convert_parsed_line_into_frame(parsed_line)
83
- frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) }
84
- end
85
-
86
78
  private
87
79
 
88
80
  def convert_parsed_line_into_frame(line)
@@ -13,7 +13,7 @@ module Sentry
13
13
  end
14
14
 
15
15
  # @return [Hash]
16
- def to_hash
16
+ def to_h
17
17
  {
18
18
  values: [
19
19
  {
@@ -21,7 +21,7 @@ module Sentry
21
21
  name: @name,
22
22
  crashed: @crashed,
23
23
  current: @current,
24
- stacktrace: @stacktrace&.to_hash
24
+ stacktrace: @stacktrace&.to_h
25
25
  }
26
26
  ]
27
27
  }
@@ -29,9 +29,12 @@ module Sentry
29
29
  "sentry.address" => :server_name,
30
30
  "sentry.sdk.name" => :sdk_name,
31
31
  "sentry.sdk.version" => :sdk_version,
32
- "sentry.message.template" => :template
32
+ "sentry.message.template" => :template,
33
+ "sentry.origin" => :origin
33
34
  }
34
35
 
36
+ PARAMETER_PREFIX = "sentry.message.parameter"
37
+
35
38
  USER_ATTRIBUTES = {
36
39
  "user.id" => :user_id,
37
40
  "user.name" => :user_username,
@@ -40,9 +43,9 @@ module Sentry
40
43
 
41
44
  LEVELS = %i[trace debug info warn error fatal].freeze
42
45
 
43
- attr_accessor :level, :body, :template, :attributes, :user
46
+ attr_accessor :level, :body, :template, :attributes, :user, :origin
44
47
 
45
- attr_reader :configuration, *SERIALIZEABLE_ATTRIBUTES
48
+ attr_reader :configuration, *(SERIALIZEABLE_ATTRIBUTES - %i[level body attributes])
46
49
 
47
50
  SERIALIZERS = %i[
48
51
  attributes
@@ -51,6 +54,7 @@ module Sentry
51
54
  parent_span_id
52
55
  sdk_name
53
56
  sdk_version
57
+ template
54
58
  timestamp
55
59
  trace_id
56
60
  user_id
@@ -79,10 +83,11 @@ module Sentry
79
83
  @template = @body if is_template?
80
84
  @attributes = options[:attributes] || DEFAULT_ATTRIBUTES
81
85
  @user = options[:user] || {}
86
+ @origin = options[:origin]
82
87
  @contexts = {}
83
88
  end
84
89
 
85
- def to_hash
90
+ def to_h
86
91
  SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo|
87
92
  memo[name] = serialize(name)
88
93
  end
@@ -146,6 +151,10 @@ module Sentry
146
151
  user[:email]
147
152
  end
148
153
 
154
+ def serialize_template
155
+ template if has_parameters?
156
+ end
157
+
149
158
  def serialize_attributes
150
159
  hash = {}
151
160
 
@@ -185,11 +194,11 @@ module Sentry
185
194
 
186
195
  if parameters.is_a?(Hash)
187
196
  parameters.each do |key, value|
188
- attributes["sentry.message.parameter.#{key}"] = value
197
+ attributes["#{PARAMETER_PREFIX}.#{key}"] = value
189
198
  end
190
199
  else
191
200
  parameters.each_with_index do |param, index|
192
- attributes["sentry.message.parameter.#{index}"] = param
201
+ attributes["#{PARAMETER_PREFIX}.#{index}"] = param
193
202
  end
194
203
  end
195
204
  end
@@ -202,5 +211,9 @@ module Sentry
202
211
  def is_template?
203
212
  body.include?("%s") || TOKEN_REGEXP.match?(body)
204
213
  end
214
+
215
+ def has_parameters?
216
+ attributes.keys.any? { |key| key.start_with?(PARAMETER_PREFIX) }
217
+ end
205
218
  end
206
219
  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)