sentry-ruby 5.26.0 → 6.1.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -4
  3. data/README.md +1 -1
  4. data/lib/sentry/background_worker.rb +1 -4
  5. data/lib/sentry/backtrace/line.rb +99 -0
  6. data/lib/sentry/backtrace.rb +44 -76
  7. data/lib/sentry/breadcrumb.rb +1 -1
  8. data/lib/sentry/breadcrumb_buffer.rb +2 -2
  9. data/lib/sentry/check_in_event.rb +2 -2
  10. data/lib/sentry/client.rb +39 -89
  11. data/lib/sentry/configuration.rb +125 -78
  12. data/lib/sentry/cron/monitor_check_ins.rb +3 -3
  13. data/lib/sentry/cron/monitor_config.rb +2 -2
  14. data/lib/sentry/cron/monitor_schedule.rb +2 -2
  15. data/lib/sentry/debug_structured_logger.rb +94 -0
  16. data/lib/sentry/dsn.rb +32 -0
  17. data/lib/sentry/envelope/item.rb +1 -2
  18. data/lib/sentry/error_event.rb +3 -3
  19. data/lib/sentry/event.rb +4 -10
  20. data/lib/sentry/graphql.rb +1 -1
  21. data/lib/sentry/hub.rb +6 -5
  22. data/lib/sentry/interface.rb +1 -1
  23. data/lib/sentry/interfaces/exception.rb +2 -2
  24. data/lib/sentry/interfaces/request.rb +2 -0
  25. data/lib/sentry/interfaces/single_exception.rb +3 -3
  26. data/lib/sentry/interfaces/stacktrace.rb +3 -3
  27. data/lib/sentry/interfaces/stacktrace_builder.rb +0 -8
  28. data/lib/sentry/interfaces/threads.rb +2 -2
  29. data/lib/sentry/log_event.rb +19 -6
  30. data/lib/sentry/profiler.rb +4 -5
  31. data/lib/sentry/propagation_context.rb +55 -18
  32. data/lib/sentry/rspec.rb +1 -1
  33. data/lib/sentry/span.rb +2 -17
  34. data/lib/sentry/std_lib_logger.rb +6 -1
  35. data/lib/sentry/test_helper.rb +23 -0
  36. data/lib/sentry/transaction.rb +72 -95
  37. data/lib/sentry/transaction_event.rb +4 -9
  38. data/lib/sentry/transport/debug_transport.rb +70 -0
  39. data/lib/sentry/transport/dummy_transport.rb +1 -0
  40. data/lib/sentry/transport/http_transport.rb +9 -5
  41. data/lib/sentry/transport.rb +3 -5
  42. data/lib/sentry/utils/encoding_helper.rb +7 -0
  43. data/lib/sentry/utils/logging_helper.rb +8 -6
  44. data/lib/sentry/utils/sample_rand.rb +97 -0
  45. data/lib/sentry/vernier/profiler.rb +4 -3
  46. data/lib/sentry/version.rb +1 -1
  47. data/lib/sentry-ruby.rb +6 -30
  48. data/sentry-ruby-core.gemspec +1 -1
  49. data/sentry-ruby.gemspec +1 -1
  50. metadata +12 -18
  51. data/lib/sentry/metrics/aggregator.rb +0 -248
  52. data/lib/sentry/metrics/configuration.rb +0 -47
  53. data/lib/sentry/metrics/counter_metric.rb +0 -25
  54. data/lib/sentry/metrics/distribution_metric.rb +0 -25
  55. data/lib/sentry/metrics/gauge_metric.rb +0 -35
  56. data/lib/sentry/metrics/local_aggregator.rb +0 -53
  57. data/lib/sentry/metrics/metric.rb +0 -19
  58. data/lib/sentry/metrics/set_metric.rb +0 -28
  59. data/lib/sentry/metrics/timing.rb +0 -51
  60. data/lib/sentry/metrics.rb +0 -56
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)
data/lib/sentry/span.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require "sentry/metrics/local_aggregator"
5
4
  require "sentry/utils/uuid"
6
5
 
7
6
  module Sentry
@@ -173,8 +172,8 @@ module Sentry
173
172
  end
174
173
 
175
174
  # @return [Hash]
176
- def to_hash
177
- hash = {
175
+ def to_h
176
+ {
178
177
  trace_id: @trace_id,
179
178
  span_id: @span_id,
180
179
  parent_span_id: @parent_span_id,
@@ -187,11 +186,6 @@ module Sentry
187
186
  data: @data,
188
187
  origin: @origin
189
188
  }
190
-
191
- summary = metrics_summary
192
- hash[:_metrics_summary] = summary if summary
193
-
194
- hash
195
189
  end
196
190
 
197
191
  # Returns the span's context that can be used to embed in an Event.
@@ -307,14 +301,5 @@ module Sentry
307
301
  def set_origin(origin)
308
302
  @origin = origin
309
303
  end
310
-
311
- # Collects gauge metrics on the span for metric summaries.
312
- def metrics_local_aggregator
313
- @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new
314
- end
315
-
316
- def metrics_summary
317
- @metrics_local_aggregator&.to_hash
318
- end
319
304
  end
320
305
  end
@@ -12,11 +12,16 @@ module Sentry
12
12
  4 => :fatal
13
13
  }.freeze
14
14
 
15
+ ORIGIN = "auto.log.ruby.std_logger"
16
+
15
17
  def add(severity, message = nil, progname = nil, &block)
16
18
  result = super
17
19
 
18
20
  return unless Sentry.initialized? && Sentry.get_current_hub
19
21
 
22
+ # Only process logs that meet or exceed the logger's level
23
+ return result if severity < level
24
+
20
25
  # exclude sentry SDK logs -- to prevent recursive log action,
21
26
  # do not process internal logs again
22
27
  if message.nil? && progname != Sentry::Logger::PROGNAME
@@ -32,7 +37,7 @@ module Sentry
32
37
  message = message.to_s.strip
33
38
 
34
39
  if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
35
- Sentry.logger.send(method, message)
40
+ Sentry.logger.send(method, message, origin: ORIGIN)
36
41
  end
37
42
  end
38
43
 
@@ -2,8 +2,13 @@
2
2
 
3
3
  module Sentry
4
4
  module TestHelper
5
+ module_function
6
+
5
7
  DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
6
8
 
9
+ # Not really real, but it will be resolved as a non-local for testing needs
10
+ REAL_DSN = "https://user:pass@getsentry.io/project/42"
11
+
7
12
  # Alters the existing SDK configuration with test-suitable options. Mainly:
8
13
  # - Sets a dummy DSN instead of `nil` or an actual DSN.
9
14
  # - Sets the transport to DummyTransport, which allows easy access to the captured events.
@@ -22,6 +27,7 @@ module Sentry
22
27
  # set transport to DummyTransport, so we can easily intercept the captured events
23
28
  dummy_config.transport.transport_class = Sentry::DummyTransport
24
29
  # make sure SDK allows sending under the current environment
30
+ dummy_config.enabled_environments ||= []
25
31
  dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment)
26
32
  # disble async event sending
27
33
  dummy_config.background_worker_threads = 0
@@ -46,6 +52,8 @@ module Sentry
46
52
  def teardown_sentry_test
47
53
  return unless Sentry.initialized?
48
54
 
55
+ clear_sentry_events
56
+
49
57
  # pop testing layer created by `setup_sentry_test`
50
58
  # but keep the base layer to avoid nil-pointer errors
51
59
  # TODO: find a way to notify users if they somehow popped the test layer before calling this method
@@ -55,6 +63,21 @@ module Sentry
55
63
  Sentry::Scope.global_event_processors.clear
56
64
  end
57
65
 
66
+ def clear_sentry_events
67
+ return unless Sentry.initialized?
68
+
69
+ sentry_transport.clear if sentry_transport.respond_to?(:clear)
70
+
71
+ if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear)
72
+ sentry_logger.clear
73
+ end
74
+ end
75
+
76
+ # @return [Sentry::StructuredLogger, Sentry::DebugStructuredLogger]
77
+ def sentry_logger
78
+ Sentry.logger
79
+ end
80
+
58
81
  # @return [Transport]
59
82
  def sentry_transport
60
83
  Sentry.get_current_client.transport