sentry-ruby 5.4.2 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/Gemfile +13 -14
  4. data/README.md +11 -8
  5. data/Rakefile +8 -1
  6. data/lib/sentry/background_worker.rb +8 -1
  7. data/lib/sentry/backpressure_monitor.rb +75 -0
  8. data/lib/sentry/backtrace.rb +1 -1
  9. data/lib/sentry/baggage.rb +70 -0
  10. data/lib/sentry/breadcrumb.rb +8 -2
  11. data/lib/sentry/check_in_event.rb +60 -0
  12. data/lib/sentry/client.rb +77 -19
  13. data/lib/sentry/configuration.rb +177 -29
  14. data/lib/sentry/cron/configuration.rb +23 -0
  15. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  16. data/lib/sentry/cron/monitor_config.rb +53 -0
  17. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  18. data/lib/sentry/envelope.rb +2 -5
  19. data/lib/sentry/event.rb +7 -29
  20. data/lib/sentry/hub.rb +100 -4
  21. data/lib/sentry/integrable.rb +6 -0
  22. data/lib/sentry/interfaces/request.rb +6 -16
  23. data/lib/sentry/interfaces/single_exception.rb +13 -3
  24. data/lib/sentry/net/http.rb +37 -46
  25. data/lib/sentry/profiler.rb +233 -0
  26. data/lib/sentry/propagation_context.rb +134 -0
  27. data/lib/sentry/puma.rb +32 -0
  28. data/lib/sentry/rack/capture_exceptions.rb +4 -5
  29. data/lib/sentry/rake.rb +1 -14
  30. data/lib/sentry/redis.rb +41 -23
  31. data/lib/sentry/release_detector.rb +1 -1
  32. data/lib/sentry/scope.rb +81 -16
  33. data/lib/sentry/session.rb +5 -7
  34. data/lib/sentry/span.rb +57 -10
  35. data/lib/sentry/test_helper.rb +19 -11
  36. data/lib/sentry/transaction.rb +183 -30
  37. data/lib/sentry/transaction_event.rb +51 -0
  38. data/lib/sentry/transport/configuration.rb +74 -1
  39. data/lib/sentry/transport/http_transport.rb +68 -37
  40. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  41. data/lib/sentry/transport.rb +39 -24
  42. data/lib/sentry/utils/argument_checking_helper.rb +9 -3
  43. data/lib/sentry/utils/encoding_helper.rb +22 -0
  44. data/lib/sentry/version.rb +1 -1
  45. data/lib/sentry-ruby.rb +116 -41
  46. metadata +14 -3
  47. data/CODE_OF_CONDUCT.md +0 -74
@@ -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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Puma::Server)
4
+
5
+ module Sentry
6
+ module Puma
7
+ module Server
8
+ PUMA_4_AND_PRIOR = Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("5.0.0")
9
+
10
+ def lowlevel_error(e, env, status=500)
11
+ result =
12
+ if PUMA_4_AND_PRIOR
13
+ super(e, env)
14
+ else
15
+ super
16
+ end
17
+
18
+ begin
19
+ Sentry.capture_exception(e) do |scope|
20
+ scope.set_rack_env(env)
21
+ end
22
+ rescue
23
+ # if anything happens, we don't want to break the app
24
+ end
25
+
26
+ result
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ Sentry.register_patch(:puma, Sentry::Puma::Server, Puma::Server)
@@ -18,7 +18,7 @@ module Sentry
18
18
  Sentry.with_scope do |scope|
19
19
  Sentry.with_session_tracking do
20
20
  scope.clear_breadcrumbs
21
- scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
21
+ scope.set_transaction_name(env["PATH_INFO"], source: :url) if env["PATH_INFO"]
22
22
  scope.set_rack_env(env)
23
23
 
24
24
  transaction = start_transaction(env, scope)
@@ -52,7 +52,7 @@ module Sentry
52
52
  end
53
53
 
54
54
  def transaction_op
55
- "rack.request".freeze
55
+ "http.server".freeze
56
56
  end
57
57
 
58
58
  def capture_exception(exception, env)
@@ -62,9 +62,8 @@ module Sentry
62
62
  end
63
63
 
64
64
  def start_transaction(env, scope)
65
- sentry_trace = env["HTTP_SENTRY_TRACE"]
66
- options = { name: scope.transaction_name, op: transaction_op }
67
- transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
65
+ options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
66
+ transaction = Sentry.continue_trace(env, **options)
68
67
  Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
69
68
  end
70
69
 
data/lib/sentry/rake.rb CHANGED
@@ -10,22 +10,13 @@ module Sentry
10
10
  def display_error_message(ex)
11
11
  Sentry.capture_exception(ex) do |scope|
12
12
  task_name = top_level_tasks.join(' ')
13
- scope.set_transaction_name(task_name)
13
+ scope.set_transaction_name(task_name, source: :task)
14
14
  scope.set_tag("rake_task", task_name)
15
15
  end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration
16
16
 
17
17
  super
18
18
  end
19
19
  end
20
-
21
- module Task
22
- # @api private
23
- def execute(args=nil)
24
- return super unless Sentry.initialized? && Sentry.get_current_hub
25
-
26
- super
27
- end
28
- end
29
20
  end
30
21
  end
31
22
 
@@ -34,8 +25,4 @@ module Rake
34
25
  class Application
35
26
  prepend(Sentry::Rake::Application)
36
27
  end
37
-
38
- class Task
39
- prepend(Sentry::Rake::Task)
40
- end
41
28
  end
data/lib/sentry/redis.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Sentry
4
4
  # @api private
5
5
  class Redis
6
- OP_NAME = "db.redis.command"
6
+ OP_NAME = "db.redis"
7
7
  LOGGER_NAME = :redis_logger
8
8
 
9
9
  def initialize(commands, host, port, db)
@@ -13,9 +13,17 @@ module Sentry
13
13
  def instrument
14
14
  return yield unless Sentry.initialized?
15
15
 
16
- record_span do
16
+ Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |span|
17
17
  yield.tap do
18
18
  record_breadcrumb
19
+
20
+ if span
21
+ span.set_description(commands_description)
22
+ span.set_data(Span::DataConventions::DB_SYSTEM, "redis")
23
+ span.set_data(Span::DataConventions::DB_NAME, db)
24
+ span.set_data(Span::DataConventions::SERVER_ADDRESS, host)
25
+ span.set_data(Span::DataConventions::SERVER_PORT, port)
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -24,19 +32,8 @@ module Sentry
24
32
 
25
33
  attr_reader :commands, :host, :port, :db
26
34
 
27
- def record_span
28
- return yield unless (transaction = Sentry.get_current_scope.get_transaction) && transaction.sampled
29
-
30
- sentry_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
31
-
32
- yield.tap do
33
- sentry_span.set_description(commands_description)
34
- sentry_span.set_data(:server, server_description)
35
- sentry_span.set_timestamp(Sentry.utc_now.to_f)
36
- end
37
- end
38
-
39
35
  def record_breadcrumb
36
+ return unless Sentry.initialized?
40
37
  return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
41
38
 
42
39
  Sentry.add_breadcrumb(
@@ -61,10 +58,16 @@ module Sentry
61
58
  def parsed_commands
62
59
  commands.map do |statement|
63
60
  command, key, *arguments = statement
61
+ command_set = { command: command.to_s.upcase }
62
+ command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
64
63
 
65
- { command: command.to_s.upcase, key: key }.tap do |command_set|
66
- command_set[:arguments] = arguments.join(" ") if Sentry.configuration.send_default_pii
64
+ if Sentry.configuration.send_default_pii
65
+ command_set[:arguments] = arguments
66
+ .select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
67
+ .join(" ")
67
68
  end
69
+
70
+ command_set
68
71
  end
69
72
  end
70
73
 
@@ -72,19 +75,34 @@ module Sentry
72
75
  "#{host}:#{port}/#{db}"
73
76
  end
74
77
 
75
- module Client
78
+ module OldClientPatch
76
79
  def logging(commands, &block)
77
- Sentry::Redis.new(commands, host, port, db).instrument do
78
- super
79
- end
80
+ Sentry::Redis.new(commands, host, port, db).instrument { super }
81
+ end
82
+ end
83
+
84
+ module GlobalRedisInstrumentation
85
+ def call(command, redis_config)
86
+ Sentry::Redis
87
+ .new([command], redis_config.host, redis_config.port, redis_config.db)
88
+ .instrument { super }
89
+ end
90
+
91
+ def call_pipelined(commands, redis_config)
92
+ Sentry::Redis
93
+ .new(commands, redis_config.host, redis_config.port, redis_config.db)
94
+ .instrument { super }
80
95
  end
81
96
  end
82
97
  end
83
98
  end
84
99
 
85
100
  if defined?(::Redis::Client)
86
- Sentry.register_patch do
87
- patch = Sentry::Redis::Client
88
- Redis::Client.prepend(patch) unless Redis::Client.ancestors.include?(patch)
101
+ if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
102
+ Sentry.register_patch(:redis, Sentry::Redis::OldClientPatch, ::Redis::Client)
103
+ elsif defined?(RedisClient)
104
+ Sentry.register_patch(:redis) do
105
+ RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
106
+ end
89
107
  end
90
108
  end
@@ -28,7 +28,7 @@ module Sentry
28
28
  end
29
29
 
30
30
  def detect_release_from_git
31
- Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
31
+ Sentry.sys_command("git rev-parse HEAD") if File.directory?(".git")
32
32
  end
33
33
 
34
34
  def detect_release_from_env
data/lib/sentry/scope.rb CHANGED
@@ -1,13 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sentry/breadcrumb_buffer"
4
+ require "sentry/propagation_context"
4
5
  require "etc"
5
6
 
6
7
  module Sentry
7
8
  class Scope
8
9
  include ArgumentCheckingHelper
9
10
 
10
- ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session]
11
+ ATTRIBUTES = [
12
+ :transaction_names,
13
+ :transaction_sources,
14
+ :contexts,
15
+ :extra,
16
+ :tags,
17
+ :user,
18
+ :level,
19
+ :breadcrumbs,
20
+ :fingerprint,
21
+ :event_processors,
22
+ :rack_env,
23
+ :span,
24
+ :session,
25
+ :propagation_context
26
+ ]
11
27
 
12
28
  attr_reader(*ATTRIBUTES)
13
29
 
@@ -28,23 +44,30 @@ module Sentry
28
44
  # @param hint [Hash] the hint data that'll be passed to event processors.
29
45
  # @return [Event]
30
46
  def apply_to_event(event, hint = nil)
31
- event.tags = tags.merge(event.tags)
32
- event.user = user.merge(event.user)
33
- event.extra = extra.merge(event.extra)
34
- event.contexts = contexts.merge(event.contexts)
35
- event.transaction = transaction_name if transaction_name
47
+ unless event.is_a?(CheckInEvent)
48
+ event.tags = tags.merge(event.tags)
49
+ event.user = user.merge(event.user)
50
+ event.extra = extra.merge(event.extra)
51
+ event.contexts = contexts.merge(event.contexts)
52
+ event.transaction = transaction_name if transaction_name
53
+ event.transaction_info = { source: transaction_source } if transaction_source
54
+ event.fingerprint = fingerprint
55
+ event.level = level
56
+ event.breadcrumbs = breadcrumbs
57
+ event.rack_env = rack_env if rack_env
58
+ end
36
59
 
37
60
  if span
38
- event.contexts[:trace] = span.get_trace_context
61
+ event.contexts[:trace] ||= span.get_trace_context
62
+ else
63
+ event.contexts[:trace] ||= propagation_context.get_trace_context
64
+ event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
39
65
  end
40
66
 
41
- event.fingerprint = fingerprint
42
- event.level = level
43
- event.breadcrumbs = breadcrumbs
44
- event.rack_env = rack_env if rack_env
67
+ all_event_processors = self.class.global_event_processors + @event_processors
45
68
 
46
- unless @event_processors.empty?
47
- @event_processors.each do |processor_block|
69
+ unless all_event_processors.empty?
70
+ all_event_processors.each do |processor_block|
48
71
  event = processor_block.call(event, hint)
49
72
  end
50
73
  end
@@ -73,10 +96,12 @@ module Sentry
73
96
  copy.extra = extra.deep_dup
74
97
  copy.tags = tags.deep_dup
75
98
  copy.user = user.deep_dup
76
- copy.transaction_names = transaction_names.deep_dup
99
+ copy.transaction_names = transaction_names.dup
100
+ copy.transaction_sources = transaction_sources.dup
77
101
  copy.fingerprint = fingerprint.deep_dup
78
102
  copy.span = span.deep_dup
79
103
  copy.session = session.deep_dup
104
+ copy.propagation_context = propagation_context.deep_dup
80
105
  copy
81
106
  end
82
107
 
@@ -90,8 +115,10 @@ module Sentry
90
115
  self.tags = scope.tags
91
116
  self.user = scope.user
92
117
  self.transaction_names = scope.transaction_names
118
+ self.transaction_sources = scope.transaction_sources
93
119
  self.fingerprint = scope.fingerprint
94
120
  self.span = scope.span
121
+ self.propagation_context = scope.propagation_context
95
122
  end
96
123
 
97
124
  # Updates the scope's data from the given options.
@@ -173,6 +200,10 @@ module Sentry
173
200
  # @return [Hash]
174
201
  def set_contexts(contexts_hash)
175
202
  check_argument_type!(contexts_hash, Hash)
203
+ contexts_hash.values.each do |val|
204
+ check_argument_type!(val, Hash)
205
+ end
206
+
176
207
  @contexts.merge!(contexts_hash) do |key, old, new|
177
208
  old.merge(new)
178
209
  end
@@ -195,8 +226,9 @@ module Sentry
195
226
  # The "transaction" here does not refer to `Transaction` objects.
196
227
  # @param transaction_name [String]
197
228
  # @return [void]
198
- def set_transaction_name(transaction_name)
229
+ def set_transaction_name(transaction_name, source: :custom)
199
230
  @transaction_names << transaction_name
231
+ @transaction_sources << source
200
232
  end
201
233
 
202
234
  # Sets the currently active session on the scope.
@@ -213,6 +245,13 @@ module Sentry
213
245
  @transaction_names.last
214
246
  end
215
247
 
248
+ # Returns current transaction source.
249
+ # The "transaction" here does not refer to `Transaction` objects.
250
+ # @return [String, nil]
251
+ def transaction_source
252
+ @transaction_sources.last
253
+ end
254
+
216
255
  # Returns the associated Transaction object.
217
256
  # @return [Transaction, nil]
218
257
  def get_transaction
@@ -241,6 +280,13 @@ module Sentry
241
280
  @event_processors << block
242
281
  end
243
282
 
283
+ # Generate a new propagation context either from the incoming env headers or from scratch.
284
+ # @param env [Hash, nil]
285
+ # @return [void]
286
+ def generate_propagation_context(env = nil)
287
+ @propagation_context = PropagationContext.new(self, env)
288
+ end
289
+
244
290
  protected
245
291
 
246
292
  # for duplicating scopes internally
@@ -256,10 +302,12 @@ module Sentry
256
302
  @level = :error
257
303
  @fingerprint = []
258
304
  @transaction_names = []
305
+ @transaction_sources = []
259
306
  @event_processors = []
260
307
  @rack_env = {}
261
308
  @span = nil
262
309
  @session = nil
310
+ generate_propagation_context
263
311
  set_new_breadcrumb_buffer
264
312
  end
265
313
 
@@ -277,7 +325,8 @@ module Sentry
277
325
  name: uname[:sysname] || RbConfig::CONFIG["host_os"],
278
326
  version: uname[:version],
279
327
  build: uname[:release],
280
- kernel_version: uname[:version]
328
+ kernel_version: uname[:version],
329
+ machine: uname[:machine]
281
330
  }
282
331
  end
283
332
  end
@@ -289,6 +338,22 @@ module Sentry
289
338
  version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v")
290
339
  }
291
340
  end
341
+
342
+ # Returns the global event processors array.
343
+ # @return [Array<Proc>]
344
+ def global_event_processors
345
+ @global_event_processors ||= []
346
+ end
347
+
348
+ # Adds a new global event processor [Proc].
349
+ # Sometimes we need a global event processor without needing to configure scope.
350
+ # These run before scope event processors.
351
+ #
352
+ # @param block [Proc]
353
+ # @return [void]
354
+ def add_global_event_processor(&block)
355
+ global_event_processors << block
356
+ end
292
357
  end
293
358
 
294
359
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Sentry
4
4
  class Session
5
- attr_reader :started, :status
5
+ attr_reader :started, :status, :aggregation_key
6
6
 
7
7
  # TODO-neel add :crashed after adding handled mechanism
8
8
  STATUSES = %i(ok errored exited)
@@ -11,6 +11,10 @@ module Sentry
11
11
  def initialize
12
12
  @started = Sentry.utc_now
13
13
  @status = :ok
14
+
15
+ # truncate seconds from the timestamp since we only care about
16
+ # minute level granularity for aggregation
17
+ @aggregation_key = Time.utc(@started.year, @started.month, @started.day, @started.hour, @started.min)
14
18
  end
15
19
 
16
20
  # TODO-neel add :crashed after adding handled mechanism
@@ -22,12 +26,6 @@ module Sentry
22
26
  @status = :exited if @status == :ok
23
27
  end
24
28
 
25
- # truncate seconds from the timestamp since we only care about
26
- # minute level granularity for aggregation
27
- def aggregation_key
28
- Time.utc(started.year, started.month, started.day, started.hour, started.min)
29
- end
30
-
31
29
  def deep_dup
32
30
  dup
33
31
  end