sentry-ruby 5.4.2 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
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