sentry-ruby 5.4.2 → 5.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 310e7ac901b3da90fb400710f95be183627b1e35587f059810669469deb27763
4
- data.tar.gz: 75cff84d0f7238ac444b5f6ce0a67f0150daefa38d9bac9e2b083a6f98ecff6e
3
+ metadata.gz: a60b754ccac4d1fd8cf47cd94d8896c46dc8232f87d06132681a19ed223ac14c
4
+ data.tar.gz: 923f906cd698d18f2fcb419bfeb2d1262c4165e6a8ffe2484de7027a90e69644
5
5
  SHA512:
6
- metadata.gz: 76251bc9b7499bc40b30c1e20c3b638a3cc5bf0a3c5df9f5210fa2b2293a3ee7e6c8c7a1d3073b1055c8788855531c36ece0517ea38e203367c0c8bf6842e130
7
- data.tar.gz: cc3b7a551b8d8c798c5843b895710244857bcf110e57cc4dd1de345e7fe6e09201bf3adf3e4645d9101df201cab8ce4cda0b547b88d137ef1faab8e7a6f1693f
6
+ metadata.gz: 7d46f719bd4e0abb6f9fcd254b6e6321c9796c2278bc31fa5f86dc541cc0ce54a4f6cc10078d6d0277c89ff0cd4896e2a0ec0aa04e3052214b01b85cb2aa115c
7
+ data.tar.gz: 14b28401b32d9ce9ee7ac0351de736d0d9f11221010e61091e4976b71a58b1fef61ca4e282ec31a714e040148197c5fab46f0a0e39e475ea1f6dd0196b5f104d
data/Gemfile CHANGED
@@ -3,12 +3,13 @@ git_source(:github) { |name| "https://github.com/#{name}.git" }
3
3
 
4
4
  gem "sentry-ruby", path: "./"
5
5
 
6
- gem "rack" unless ENV["WITHOUT_RACK"] == "1"
6
+ rack_version = ENV["RACK_VERSION"]
7
+ rack_version = "3.0.0" if rack_version.nil?
8
+ gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0"
7
9
 
8
10
  gem "rake", "~> 12.0"
9
11
  gem "rspec", "~> 3.0"
10
12
  gem "rspec-retry"
11
- gem "webmock"
12
13
  gem "fakeredis"
13
14
  gem "timecop"
14
15
  gem 'simplecov'
data/README.md CHANGED
@@ -20,6 +20,7 @@ Sentry SDK for Ruby
20
20
  | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-sidekiq%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_sidekiq_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-sidekiq.svg)](https://rubygems.org/gems/sentry-sidekiq/) |
21
21
  | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-delayed_job%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_delayed_job_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-delayed_job.svg)](https://rubygems.org/gems/sentry-delayed_job/) |
22
22
  | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-resque%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_resque_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-resque.svg)](https://rubygems.org/gems/sentry-resque/) |
23
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-opentelemetry?label=sentry-opentelemetry)](https://rubygems.org/gems/sentry-opentelemetry) | [![Build Status](https://github.com/getsentry/sentry-ruby/workflows/sentry-opentelemetry%20Test/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/sentry_opentelemetry_test.yml) | [![Coverage Status](https://img.shields.io/codecov/c/github/getsentry/sentry-ruby/master?logo=codecov)](https://codecov.io/gh/getsentry/sentry-ruby/branch/master) | [![Downloads](https://img.shields.io/gem/dt/sentry-opentelemetry.svg)](https://rubygems.org/gems/sentry-opentelemetry/) |
23
24
 
24
25
 
25
26
 
@@ -51,6 +52,7 @@ gem "sentry-rails"
51
52
  gem "sentry-sidekiq"
52
53
  gem "sentry-delayed_job"
53
54
  gem "sentry-resque"
55
+ gem "sentry-opentelemetry"
54
56
  ```
55
57
 
56
58
  ### Configuration
@@ -88,6 +90,7 @@ To learn more about sampling transactions, please visit the [official documentat
88
90
  - [Sidekiq](https://docs.sentry.io/platforms/ruby/guides/sidekiq/)
89
91
  - [DelayedJob](https://docs.sentry.io/platforms/ruby/guides/delayed_job/)
90
92
  - [Resque](https://docs.sentry.io/platforms/ruby/guides/resque/)
93
+ - [OpenTemeletry](https://docs.sentry.io/platforms/ruby/performance/instrumentation/opentelemetry/)
91
94
 
92
95
  ### Enriching Events
93
96
 
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Sentry
6
+ # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation.
7
+ class Baggage
8
+ SENTRY_PREFIX = 'sentry-'
9
+ SENTRY_PREFIX_REGEX = /^sentry-/.freeze
10
+
11
+ DSC_KEYS = %w(
12
+ trace_id
13
+ public_key
14
+ sample_rate
15
+ release
16
+ environment
17
+ transaction
18
+ user_id
19
+ user_segment
20
+ ).freeze
21
+
22
+ # @return [Hash]
23
+ attr_reader :items
24
+
25
+ # @return [Boolean]
26
+ attr_reader :mutable
27
+
28
+ def initialize(items, mutable: true)
29
+ @items = items
30
+ @mutable = mutable
31
+ end
32
+
33
+ # Creates a Baggage object from an incoming W3C Baggage header string.
34
+ #
35
+ # Sentry items are identified with the 'sentry-' prefix and stored in a hash.
36
+ # The presence of a Sentry item makes the baggage object immutable.
37
+ #
38
+ # @param header [String] The incoming Baggage header string.
39
+ # @return [Baggage, nil]
40
+ def self.from_incoming_header(header)
41
+ items = {}
42
+ mutable = true
43
+
44
+ header.split(',').each do |item|
45
+ item = item.strip
46
+ key, val = item.split('=')
47
+
48
+ next unless key && val
49
+ next unless key =~ SENTRY_PREFIX_REGEX
50
+
51
+ baggage_key = key.split('-')[1]
52
+ next unless baggage_key
53
+
54
+ items[CGI.unescape(baggage_key)] = CGI.unescape(val)
55
+ mutable = false
56
+ end
57
+
58
+ new(items, mutable: mutable)
59
+ end
60
+
61
+ # Make the Baggage immutable.
62
+ # @return [void]
63
+ def freeze!
64
+ @mutable = false
65
+ end
66
+
67
+ # A {https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#envelope-header Dynamic Sampling Context}
68
+ # hash to be used in the trace envelope header.
69
+ # @return [Hash]
70
+ def dynamic_sampling_context
71
+ @items.select { |k, _v| DSC_KEYS.include?(k) }
72
+ end
73
+
74
+ # Serialize the Baggage object back to a string.
75
+ # @return [String]
76
+ def serialize
77
+ items = @items.map { |k, v| "#{SENTRY_PREFIX}#{CGI.escape(k)}=#{CGI.escape(v)}" }
78
+ items.join(',')
79
+ end
80
+ end
81
+ end
data/lib/sentry/client.rb CHANGED
@@ -105,16 +105,7 @@ module Sentry
105
105
  # @param transaction [Transaction] the transaction to be recorded.
106
106
  # @return [TransactionEvent]
107
107
  def event_from_transaction(transaction)
108
- TransactionEvent.new(configuration: configuration).tap do |event|
109
- event.transaction = transaction.name
110
- event.contexts.merge!(trace: transaction.get_trace_context)
111
- event.timestamp = transaction.timestamp
112
- event.start_timestamp = transaction.start_timestamp
113
- event.tags = transaction.tags
114
-
115
- finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
116
- event.spans = finished_spans.map(&:to_hash)
117
- end
108
+ TransactionEvent.new(configuration: configuration, transaction: transaction)
118
109
  end
119
110
 
120
111
  # @!macro send_event
@@ -156,6 +147,22 @@ module Sentry
156
147
  trace
157
148
  end
158
149
 
150
+ # Generates a W3C Baggage header for distribted tracing from the given Span.
151
+ # Returns `nil` if `config.propagate_traces` is `false`.
152
+ # @param span [Span] the span to generate trace from.
153
+ # @return [String, nil]
154
+ def generate_baggage(span)
155
+ return unless configuration.propagate_traces
156
+
157
+ baggage = span.to_baggage
158
+
159
+ if baggage && !baggage.empty?
160
+ log_debug("[Tracing] Adding #{BAGGAGE_HEADER_NAME} header to outgoing request: #{baggage}")
161
+ end
162
+
163
+ baggage
164
+ end
165
+
159
166
  private
160
167
 
161
168
  def dispatch_background_event(event, hint)
@@ -211,6 +211,10 @@ module Sentry
211
211
  # @return [Boolean]
212
212
  attr_accessor :auto_session_tracking
213
213
 
214
+ # The instrumenter to use, :sentry or :otel
215
+ # @return [Symbol]
216
+ attr_reader :instrumenter
217
+
214
218
  # these are not config options
215
219
  # @!visibility private
216
220
  attr_reader :errors, :gem_specs
@@ -237,6 +241,8 @@ module Sentry
237
241
  MODULE_SEPARATOR = "::".freeze
238
242
  SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder]
239
243
 
244
+ INSTRUMENTERS = [:sentry, :otel]
245
+
240
246
  # Post initialization callbacks are called at the end of initialization process
241
247
  # allowing extending the configuration of sentry-ruby by multiple extensions
242
248
  @@post_initialization_callbacks = []
@@ -269,6 +275,7 @@ module Sentry
269
275
  self.trusted_proxies = []
270
276
  self.dsn = ENV['SENTRY_DSN']
271
277
  self.server_name = server_name_from_env
278
+ self.instrumenter = :sentry
272
279
 
273
280
  self.before_send = nil
274
281
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
@@ -332,6 +339,10 @@ module Sentry
332
339
  @environment = environment.to_s
333
340
  end
334
341
 
342
+ def instrumenter=(instrumenter)
343
+ @instrumenter = INSTRUMENTERS.include?(instrumenter) ? instrumenter : :sentry
344
+ end
345
+
335
346
  def sending_allowed?
336
347
  @errors = []
337
348
 
data/lib/sentry/event.rb CHANGED
@@ -18,7 +18,7 @@ module Sentry
18
18
  event_id level timestamp
19
19
  release environment server_name modules
20
20
  message user tags contexts extra
21
- fingerprint breadcrumbs transaction
21
+ fingerprint breadcrumbs transaction transaction_info
22
22
  platform sdk type
23
23
  )
24
24
 
data/lib/sentry/hub.rb CHANGED
@@ -76,8 +76,9 @@ module Sentry
76
76
  @stack.pop
77
77
  end
78
78
 
79
- def start_transaction(transaction: nil, custom_sampling_context: {}, **options)
79
+ def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options)
80
80
  return unless configuration.tracing_enabled?
81
+ return unless instrumenter == configuration.instrumenter
81
82
 
82
83
  transaction ||= Transaction.new(**options.merge(hub: self))
83
84
 
@@ -92,6 +93,26 @@ module Sentry
92
93
  transaction
93
94
  end
94
95
 
96
+ def with_child_span(instrumenter: :sentry, **attributes, &block)
97
+ return yield(nil) unless instrumenter == configuration.instrumenter
98
+
99
+ current_span = current_scope.get_span
100
+ return yield(nil) unless current_span
101
+
102
+ result = nil
103
+
104
+ begin
105
+ current_span.with_child_span(**attributes) do |child_span|
106
+ current_scope.set_span(child_span)
107
+ result = yield(child_span)
108
+ end
109
+ ensure
110
+ current_scope.set_span(current_span)
111
+ end
112
+
113
+ result
114
+ end
115
+
95
116
  def capture_exception(exception, **options, &block)
96
117
  check_argument_type!(exception, ::Exception)
97
118
 
@@ -73,7 +73,7 @@ module Sentry
73
73
  request.POST
74
74
  elsif request.body # JSON requests, etc
75
75
  data = request.body.read(MAX_BODY_LIMIT)
76
- data = encode_to_utf_8(data.to_s)
76
+ data = Utils::EncodingHelper.encode_to_utf_8(data.to_s)
77
77
  request.body.rewind
78
78
  data
79
79
  end
@@ -94,7 +94,7 @@ module Sentry
94
94
  key = key.sub(/^HTTP_/, "")
95
95
  key = key.split('_').map(&:capitalize).join('-')
96
96
 
97
- memo[key] = encode_to_utf_8(value.to_s)
97
+ memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
98
98
  rescue StandardError => e
99
99
  # Rails adds objects to the Rack env that can sometimes raise exceptions
100
100
  # when `to_s` is called.
@@ -105,31 +105,21 @@ module Sentry
105
105
  end
106
106
  end
107
107
 
108
- def encode_to_utf_8(value)
109
- if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
110
- value = value.dup.force_encoding(Encoding::UTF_8)
111
- end
112
-
113
- if !value.valid_encoding?
114
- value = value.scrub
115
- end
116
-
117
- value
118
- end
119
-
120
108
  def is_skippable_header?(key)
121
109
  key.upcase != key || # lower-case envs aren't real http headers
122
110
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
123
111
  !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
124
112
  end
125
113
 
126
- # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
114
+ # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
127
115
  # to think this is a Version header. Instead, this is mapped to
128
116
  # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
129
117
  # if the request has legitimately sent a Version header themselves.
130
118
  # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
131
- # NOTE: This will be removed in version 3.0+
132
119
  def is_server_protocol?(key, value, protocol_version)
120
+ rack_version = Gem::Version.new(::Rack.release)
121
+ return false if rack_version >= Gem::Version.new("3.0")
122
+
133
123
  key == 'HTTP_VERSION' && value == protocol_version
134
124
  end
135
125
 
@@ -15,7 +15,15 @@ module Sentry
15
15
 
16
16
  def initialize(exception:, stacktrace: nil)
17
17
  @type = exception.class.to_s
18
- @value = (exception.message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
18
+ exception_message =
19
+ if exception.respond_to?(:detailed_message)
20
+ exception.detailed_message(highlight: false)
21
+ else
22
+ exception.message || ""
23
+ end
24
+
25
+ @value = exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
26
+
19
27
  @module = exception.class.to_s.split('::')[0...-1].join('::')
20
28
  @thread_id = Thread.current.object_id
21
29
  @stacktrace = stacktrace
@@ -26,14 +26,21 @@ module Sentry
26
26
  #
27
27
  # So we're only instrumenting request when `Net::HTTP` is already started
28
28
  def request(req, body = nil, &block)
29
- return super unless started?
29
+ return super unless started? && Sentry.initialized?
30
+ return super if from_sentry_sdk?
30
31
 
31
- sentry_span = start_sentry_span
32
- set_sentry_trace_header(req, sentry_span)
32
+ 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)
33
34
 
34
- super.tap do |res|
35
- record_sentry_breadcrumb(req, res)
36
- record_sentry_span(req, res, sentry_span)
35
+ super.tap do |res|
36
+ record_sentry_breadcrumb(req, res)
37
+
38
+ if sentry_span
39
+ request_info = extract_request_info(req)
40
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
41
+ sentry_span.set_data(:status, res.code.to_i)
42
+ end
43
+ end
37
44
  end
38
45
  end
39
46
 
@@ -42,13 +49,17 @@ module Sentry
42
49
  def set_sentry_trace_header(req, sentry_span)
43
50
  return unless sentry_span
44
51
 
45
- trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
52
+ client = Sentry.get_current_client
53
+
54
+ trace = client.generate_sentry_trace(sentry_span)
46
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?
47
59
  end
48
60
 
49
61
  def record_sentry_breadcrumb(req, res)
50
62
  return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
51
- return if from_sentry_sdk?
52
63
 
53
64
  request_info = extract_request_info(req)
54
65
 
@@ -64,29 +75,6 @@ module Sentry
64
75
  Sentry.add_breadcrumb(crumb)
65
76
  end
66
77
 
67
- def record_sentry_span(req, res, sentry_span)
68
- return unless Sentry.initialized? && sentry_span
69
-
70
- request_info = extract_request_info(req)
71
- sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
72
- sentry_span.set_data(:status, res.code.to_i)
73
- finish_sentry_span(sentry_span)
74
- end
75
-
76
- def start_sentry_span
77
- return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span
78
- return if from_sentry_sdk?
79
- return if span.sampled == false
80
-
81
- span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
82
- end
83
-
84
- def finish_sentry_span(sentry_span)
85
- return unless Sentry.initialized? && sentry_span
86
-
87
- sentry_span.set_timestamp(Sentry.utc_now.to_f)
88
- end
89
-
90
78
  def from_sentry_sdk?
91
79
  dsn = Sentry.configuration.dsn
92
80
  dsn && dsn.host == self.address
@@ -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)
@@ -63,8 +63,10 @@ module Sentry
63
63
 
64
64
  def start_transaction(env, scope)
65
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
66
+ baggage = env["HTTP_BAGGAGE"]
67
+
68
+ options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
69
+ transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
68
70
  Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
69
71
  end
70
72
 
data/lib/sentry/rake.rb CHANGED
@@ -10,7 +10,7 @@ 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
 
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,14 @@ 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(:server, server_description)
23
+ end
19
24
  end
20
25
  end
21
26
  end
@@ -24,18 +29,6 @@ module Sentry
24
29
 
25
30
  attr_reader :commands, :host, :port, :db
26
31
 
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
32
  def record_breadcrumb
40
33
  return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
41
34
 
@@ -61,10 +54,15 @@ module Sentry
61
54
  def parsed_commands
62
55
  commands.map do |statement|
63
56
  command, key, *arguments = statement
57
+ command_set = { command: command.to_s.upcase, key: key }
64
58
 
65
- { command: command.to_s.upcase, key: key }.tap do |command_set|
66
- command_set[:arguments] = arguments.join(" ") if Sentry.configuration.send_default_pii
59
+ if Sentry.configuration.send_default_pii
60
+ command_set[:arguments] = arguments
61
+ .select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
62
+ .join(" ")
67
63
  end
64
+
65
+ command_set
68
66
  end
69
67
  end
70
68
 
data/lib/sentry/scope.rb CHANGED
@@ -7,7 +7,21 @@ module Sentry
7
7
  class Scope
8
8
  include ArgumentCheckingHelper
9
9
 
10
- ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session]
10
+ ATTRIBUTES = [
11
+ :transaction_names,
12
+ :transaction_sources,
13
+ :contexts,
14
+ :extra,
15
+ :tags,
16
+ :user,
17
+ :level,
18
+ :breadcrumbs,
19
+ :fingerprint,
20
+ :event_processors,
21
+ :rack_env,
22
+ :span,
23
+ :session
24
+ ]
11
25
 
12
26
  attr_reader(*ATTRIBUTES)
13
27
 
@@ -33,6 +47,7 @@ module Sentry
33
47
  event.extra = extra.merge(event.extra)
34
48
  event.contexts = contexts.merge(event.contexts)
35
49
  event.transaction = transaction_name if transaction_name
50
+ event.transaction_info = { source: transaction_source } if transaction_source
36
51
 
37
52
  if span
38
53
  event.contexts[:trace] = span.get_trace_context
@@ -73,7 +88,8 @@ module Sentry
73
88
  copy.extra = extra.deep_dup
74
89
  copy.tags = tags.deep_dup
75
90
  copy.user = user.deep_dup
76
- copy.transaction_names = transaction_names.deep_dup
91
+ copy.transaction_names = transaction_names.dup
92
+ copy.transaction_sources = transaction_sources.dup
77
93
  copy.fingerprint = fingerprint.deep_dup
78
94
  copy.span = span.deep_dup
79
95
  copy.session = session.deep_dup
@@ -90,6 +106,7 @@ module Sentry
90
106
  self.tags = scope.tags
91
107
  self.user = scope.user
92
108
  self.transaction_names = scope.transaction_names
109
+ self.transaction_sources = scope.transaction_sources
93
110
  self.fingerprint = scope.fingerprint
94
111
  self.span = scope.span
95
112
  end
@@ -195,8 +212,9 @@ module Sentry
195
212
  # The "transaction" here does not refer to `Transaction` objects.
196
213
  # @param transaction_name [String]
197
214
  # @return [void]
198
- def set_transaction_name(transaction_name)
215
+ def set_transaction_name(transaction_name, source: :custom)
199
216
  @transaction_names << transaction_name
217
+ @transaction_sources << source
200
218
  end
201
219
 
202
220
  # Sets the currently active session on the scope.
@@ -213,6 +231,13 @@ module Sentry
213
231
  @transaction_names.last
214
232
  end
215
233
 
234
+ # Returns current transaction source.
235
+ # The "transaction" here does not refer to `Transaction` objects.
236
+ # @return [String, nil]
237
+ def transaction_source
238
+ @transaction_sources.last
239
+ end
240
+
216
241
  # Returns the associated Transaction object.
217
242
  # @return [Transaction, nil]
218
243
  def get_transaction
@@ -256,6 +281,7 @@ module Sentry
256
281
  @level = :error
257
282
  @fingerprint = []
258
283
  @transaction_names = []
284
+ @transaction_sources = []
259
285
  @event_processors = []
260
286
  @rack_env = {}
261
287
  @span = nil
@@ -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
data/lib/sentry/span.rb CHANGED
@@ -60,25 +60,28 @@ module Sentry
60
60
  # The Transaction object the Span belongs to.
61
61
  # Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction.
62
62
  # @return [Transaction]
63
- attr_accessor :transaction
63
+ attr_reader :transaction
64
64
 
65
65
  def initialize(
66
+ transaction:,
66
67
  description: nil,
67
68
  op: nil,
68
69
  status: nil,
69
70
  trace_id: nil,
71
+ span_id: nil,
70
72
  parent_span_id: nil,
71
73
  sampled: nil,
72
74
  start_timestamp: nil,
73
75
  timestamp: nil
74
76
  )
75
77
  @trace_id = trace_id || SecureRandom.uuid.delete("-")
76
- @span_id = SecureRandom.hex(8)
78
+ @span_id = span_id || SecureRandom.hex(8)
77
79
  @parent_span_id = parent_span_id
78
80
  @sampled = sampled
79
81
  @start_timestamp = start_timestamp || Sentry.utc_now.to_f
80
82
  @timestamp = timestamp
81
83
  @description = description
84
+ @transaction = transaction
82
85
  @op = op
83
86
  @status = status
84
87
  @data = {}
@@ -87,11 +90,8 @@ module Sentry
87
90
 
88
91
  # Finishes the span by adding a timestamp.
89
92
  # @return [self]
90
- def finish
91
- # already finished
92
- return if @timestamp
93
-
94
- @timestamp = Sentry.utc_now.to_f
93
+ def finish(end_timestamp: nil)
94
+ @timestamp = end_timestamp || @timestamp || Sentry.utc_now.to_f
95
95
  self
96
96
  end
97
97
 
@@ -104,6 +104,13 @@ module Sentry
104
104
  "#{@trace_id}-#{@span_id}-#{sampled_flag}"
105
105
  end
106
106
 
107
+ # Generates a W3C Baggage header string for distributed tracing
108
+ # from the incoming baggage stored on the transaction.
109
+ # @return [String, nil]
110
+ def to_baggage
111
+ transaction.get_baggage&.serialize
112
+ end
113
+
107
114
  # @return [Hash]
108
115
  def to_hash
109
116
  {
@@ -136,9 +143,8 @@ module Sentry
136
143
  # Starts a child span with given attributes.
137
144
  # @param attributes [Hash] the attributes for the child span.
138
145
  def start_child(**attributes)
139
- attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
146
+ attributes = attributes.dup.merge(transaction: @transaction, trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
140
147
  new_span = Span.new(**attributes)
141
- new_span.transaction = transaction
142
148
  new_span.span_recorder = span_recorder
143
149
 
144
150
  if span_recorder
@@ -31,6 +31,7 @@ module Sentry
31
31
 
32
32
  test_client = Sentry::Client.new(copied_config)
33
33
  Sentry.get_current_hub.bind_client(test_client)
34
+ Sentry.get_current_scope.clear
34
35
  end
35
36
 
36
37
  # Clears all stored events and envelopes.
@@ -41,6 +42,7 @@ module Sentry
41
42
 
42
43
  sentry_transport.events = []
43
44
  sentry_transport.envelopes = []
45
+ Sentry.get_current_scope.clear
44
46
  end
45
47
 
46
48
  # @return [Transport]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sentry/baggage"
4
+
3
5
  module Sentry
4
6
  class Transaction < Span
5
7
  SENTRY_TRACE_REGEXP = Regexp.new(
@@ -12,16 +14,29 @@ module Sentry
12
14
  UNLABELD_NAME = "<unlabeled transaction>".freeze
13
15
  MESSAGE_PREFIX = "[Tracing]"
14
16
 
17
+ # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
18
+ SOURCES = %i(custom url route view component task)
19
+
15
20
  include LoggingHelper
16
21
 
17
22
  # The name of the transaction.
18
23
  # @return [String]
19
24
  attr_reader :name
20
25
 
26
+ # The source of the transaction name.
27
+ # @return [Symbol]
28
+ attr_reader :source
29
+
21
30
  # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
22
31
  # @return [String]
23
32
  attr_reader :parent_sampled
24
33
 
34
+ # The parsed incoming W3C baggage header.
35
+ # This is only for accessing the current baggage variable.
36
+ # Please use the #get_baggage method for interfacing outside this class.
37
+ # @return [Baggage, nil]
38
+ attr_reader :baggage
39
+
25
40
  # @deprecated Use Sentry.get_current_hub instead.
26
41
  attr_reader :hub
27
42
 
@@ -31,18 +46,38 @@ module Sentry
31
46
  # @deprecated Use Sentry.logger instead.
32
47
  attr_reader :logger
33
48
 
34
- def initialize(name: nil, parent_sampled: nil, hub:, **options)
35
- super(**options)
49
+ # The effective sample rate at which this transaction was sampled.
50
+ # @return [Float, nil]
51
+ attr_reader :effective_sample_rate
36
52
 
37
- @name = name
53
+ # Additional contexts stored directly on the transaction object.
54
+ # @return [Hash]
55
+ attr_reader :contexts
56
+
57
+ def initialize(
58
+ hub:,
59
+ name: nil,
60
+ source: :custom,
61
+ parent_sampled: nil,
62
+ baggage: nil,
63
+ **options
64
+ )
65
+ super(transaction: self, **options)
66
+
67
+ set_name(name, source: source)
38
68
  @parent_sampled = parent_sampled
39
- @transaction = self
40
69
  @hub = hub
70
+ @baggage = baggage
41
71
  @configuration = hub.configuration # to be removed
42
72
  @tracing_enabled = hub.configuration.tracing_enabled?
43
73
  @traces_sampler = hub.configuration.traces_sampler
44
74
  @traces_sample_rate = hub.configuration.traces_sample_rate
45
75
  @logger = hub.configuration.logger
76
+ @release = hub.configuration.release
77
+ @environment = hub.configuration.environment
78
+ @dsn = hub.configuration.dsn
79
+ @effective_sample_rate = nil
80
+ @contexts = {}
46
81
  init_span_recorder
47
82
  end
48
83
 
@@ -52,31 +87,65 @@ module Sentry
52
87
  #
53
88
  # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
54
89
  # @param sentry_trace [String] the trace string from the previous transaction.
90
+ # @param baggage [String, nil] the incoming baggage header string.
55
91
  # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
56
92
  # @param options [Hash] the options you want to use to initialize a Transaction instance.
57
93
  # @return [Transaction, nil]
58
- def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
94
+ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
59
95
  return unless hub.configuration.tracing_enabled?
60
96
  return unless sentry_trace
61
97
 
98
+ sentry_trace_data = extract_sentry_trace(sentry_trace)
99
+ return unless sentry_trace_data
100
+
101
+ trace_id, parent_span_id, parent_sampled = sentry_trace_data
102
+
103
+ baggage = if baggage && !baggage.empty?
104
+ Baggage.from_incoming_header(baggage)
105
+ else
106
+ # If there's an incoming sentry-trace but no incoming baggage header,
107
+ # for instance in traces coming from older SDKs,
108
+ # baggage will be empty and frozen and won't be populated as head SDK.
109
+ Baggage.new({})
110
+ end
111
+
112
+ baggage.freeze!
113
+
114
+ new(
115
+ trace_id: trace_id,
116
+ parent_span_id: parent_span_id,
117
+ parent_sampled: parent_sampled,
118
+ hub: hub,
119
+ baggage: baggage,
120
+ **options
121
+ )
122
+ end
123
+
124
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
125
+ #
126
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
127
+ # @return [Array, nil]
128
+ def self.extract_sentry_trace(sentry_trace)
62
129
  match = SENTRY_TRACE_REGEXP.match(sentry_trace)
63
- return if match.nil?
64
- trace_id, parent_span_id, sampled_flag = match[1..3]
130
+ return nil if match.nil?
65
131
 
66
- parent_sampled =
67
- if sampled_flag.nil?
68
- nil
69
- else
70
- sampled_flag != "0"
71
- end
132
+ trace_id, parent_span_id, sampled_flag = match[1..3]
133
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
72
134
 
73
- new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
135
+ [trace_id, parent_span_id, parent_sampled]
74
136
  end
75
137
 
76
138
  # @return [Hash]
77
139
  def to_hash
78
140
  hash = super
79
- hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
141
+
142
+ hash.merge!(
143
+ name: @name,
144
+ source: @source,
145
+ sampled: @sampled,
146
+ parent_sampled: @parent_sampled
147
+ )
148
+
80
149
  hash
81
150
  end
82
151
 
@@ -103,7 +172,10 @@ module Sentry
103
172
  return
104
173
  end
105
174
 
106
- return unless @sampled.nil?
175
+ unless @sampled.nil?
176
+ @effective_sample_rate = @sampled ? 1.0 : 0.0
177
+ return
178
+ end
107
179
 
108
180
  sample_rate =
109
181
  if @traces_sampler.is_a?(Proc)
@@ -116,7 +188,11 @@ module Sentry
116
188
 
117
189
  transaction_description = generate_transaction_description
118
190
 
119
- unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
191
+ if [true, false].include?(sample_rate)
192
+ @effective_sample_rate = sample_rate ? 1.0 : 0.0
193
+ elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
194
+ @effective_sample_rate = sample_rate.to_f
195
+ else
120
196
  @sampled = false
121
197
  log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
122
198
  return
@@ -146,7 +222,7 @@ module Sentry
146
222
  # Finishes the transaction's recording and send it to Sentry.
147
223
  # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
148
224
  # @return [TransactionEvent]
149
- def finish(hub: nil)
225
+ def finish(hub: nil, end_timestamp: nil)
150
226
  if hub
151
227
  log_warn(
152
228
  <<~MSG
@@ -158,7 +234,7 @@ module Sentry
158
234
 
159
235
  hub ||= @hub
160
236
 
161
- super() # Span#finish doesn't take arguments
237
+ super(end_timestamp: end_timestamp)
162
238
 
163
239
  if @name.nil?
164
240
  @name = UNLABELD_NAME
@@ -172,6 +248,32 @@ module Sentry
172
248
  end
173
249
  end
174
250
 
251
+ # Get the existing frozen incoming baggage
252
+ # or populate one with sentry- items as the head SDK.
253
+ # @return [Baggage]
254
+ def get_baggage
255
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
256
+ @baggage
257
+ end
258
+
259
+ # Set the transaction name directly.
260
+ # Considered internal api since it bypasses the usual scope logic.
261
+ # @param name [String]
262
+ # @param source [Symbol]
263
+ # @return [void]
264
+ def set_name(name, source: :custom)
265
+ @name = name
266
+ @source = SOURCES.include?(source) ? source.to_sym : :custom
267
+ end
268
+
269
+ # Set contexts directly on the transaction.
270
+ # @param key [String, Symbol]
271
+ # @param value [Object]
272
+ # @return [void]
273
+ def set_context(key, value)
274
+ @contexts[key] = value
275
+ end
276
+
175
277
  protected
176
278
 
177
279
  def init_span_recorder(limit = 1000)
@@ -188,6 +290,29 @@ module Sentry
188
290
  result
189
291
  end
190
292
 
293
+ def populate_head_baggage
294
+ items = {
295
+ "trace_id" => trace_id,
296
+ "sample_rate" => effective_sample_rate&.to_s,
297
+ "environment" => @environment,
298
+ "release" => @release,
299
+ "public_key" => @dsn&.public_key
300
+ }
301
+
302
+ items["transaction"] = name unless source_low_quality?
303
+
304
+ user = @hub.current_scope&.user
305
+ items["user_segment"] = user["segment"] if user && user["segment"]
306
+
307
+ items.compact!
308
+ @baggage = Baggage.new(items, mutable: false)
309
+ end
310
+
311
+ # These are high cardinality and thus bad
312
+ def source_low_quality?
313
+ source == :url
314
+ end
315
+
191
316
  class SpanRecorder
192
317
  attr_reader :max_length, :spans
193
318
 
@@ -8,9 +8,28 @@ module Sentry
8
8
  # @return [<Array[Span]>]
9
9
  attr_accessor :spans
10
10
 
11
+ # @return [Hash, nil]
12
+ attr_accessor :dynamic_sampling_context
13
+
11
14
  # @return [Float, nil]
12
15
  attr_reader :start_timestamp
13
16
 
17
+ def initialize(transaction:, **options)
18
+ super(**options)
19
+
20
+ self.transaction = transaction.name
21
+ self.transaction_info = { source: transaction.source }
22
+ self.contexts.merge!(transaction.contexts)
23
+ self.contexts.merge!(trace: transaction.get_trace_context)
24
+ self.timestamp = transaction.timestamp
25
+ self.start_timestamp = transaction.start_timestamp
26
+ self.tags = transaction.tags
27
+ self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
28
+
29
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
30
+ self.spans = finished_spans.map(&:to_hash)
31
+ end
32
+
14
33
  # Sets the event's start_timestamp.
15
34
  # @param time [Time, Float]
16
35
  # @return [void]
@@ -136,14 +136,18 @@ module Sentry
136
136
  event_id = event_payload[:event_id] || event_payload["event_id"]
137
137
  item_type = event_payload[:type] || event_payload["type"]
138
138
 
139
- envelope = Envelope.new(
140
- {
141
- event_id: event_id,
142
- dsn: @dsn.to_s,
143
- sdk: Sentry.sdk_meta,
144
- sent_at: Sentry.utc_now.iso8601
145
- }
146
- )
139
+ envelope_headers = {
140
+ event_id: event_id,
141
+ dsn: @dsn.to_s,
142
+ sdk: Sentry.sdk_meta,
143
+ sent_at: Sentry.utc_now.iso8601
144
+ }
145
+
146
+ if event.is_a?(TransactionEvent) && event.dynamic_sampling_context
147
+ envelope_headers[:trace] = event.dynamic_sampling_context
148
+ end
149
+
150
+ envelope = Envelope.new(envelope_headers)
147
151
 
148
152
  envelope.add_item(
149
153
  { type: item_type, content_type: 'application/json' },
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Utils
5
+ module EncodingHelper
6
+ def self.encode_to_utf_8(value)
7
+ if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
8
+ value = value.dup.force_encoding(Encoding::UTF_8)
9
+ end
10
+
11
+ value = value.scrub unless value.valid_encoding?
12
+ value
13
+ end
14
+
15
+ def self.valid_utf_8?(value)
16
+ return true unless value.respond_to?(:force_encoding)
17
+
18
+ value.dup.force_encoding(Encoding::UTF_8).valid_encoding?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.4.2"
4
+ VERSION = "5.7.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -8,6 +8,7 @@ require "sentry/version"
8
8
  require "sentry/exceptions"
9
9
  require "sentry/core_ext/object/deep_dup"
10
10
  require "sentry/utils/argument_checking_helper"
11
+ require "sentry/utils/encoding_helper"
11
12
  require "sentry/utils/logging_helper"
12
13
  require "sentry/configuration"
13
14
  require "sentry/logger"
@@ -39,6 +40,8 @@ module Sentry
39
40
 
40
41
  SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze
41
42
 
43
+ BAGGAGE_HEADER_NAME = "baggage".freeze
44
+
42
45
  THREAD_LOCAL = :sentry_hub
43
46
 
44
47
  class << self
@@ -348,7 +351,7 @@ module Sentry
348
351
  # @yieldparam scope [Scope]
349
352
  # @return [void]
350
353
  def with_scope(&block)
351
- return unless initialized?
354
+ return yield unless initialized?
352
355
  get_current_hub.with_scope(&block)
353
356
  end
354
357
 
@@ -439,22 +442,8 @@ module Sentry
439
442
  # end
440
443
  #
441
444
  def with_child_span(**attributes, &block)
442
- if Sentry.initialized? && current_span = get_current_scope.get_span
443
- result = nil
444
-
445
- begin
446
- current_span.with_child_span(**attributes) do |child_span|
447
- get_current_scope.set_span(child_span)
448
- result = yield(child_span)
449
- end
450
- ensure
451
- get_current_scope.set_span(current_span)
452
- end
453
-
454
- result
455
- else
456
- yield(nil)
457
- end
445
+ return yield(nil) unless Sentry.initialized?
446
+ get_current_hub.with_child_span(**attributes, &block)
458
447
  end
459
448
 
460
449
  # Returns the id of the lastly reported Sentry::Event.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.4.2
4
+ version: 5.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-17 00:00:00.000000000 Z
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -53,6 +53,7 @@ files:
53
53
  - lib/sentry-ruby.rb
54
54
  - lib/sentry/background_worker.rb
55
55
  - lib/sentry/backtrace.rb
56
+ - lib/sentry/baggage.rb
56
57
  - lib/sentry/breadcrumb.rb
57
58
  - lib/sentry/breadcrumb/sentry_logger.rb
58
59
  - lib/sentry/breadcrumb_buffer.rb
@@ -95,6 +96,7 @@ files:
95
96
  - lib/sentry/transport/http_transport.rb
96
97
  - lib/sentry/utils/argument_checking_helper.rb
97
98
  - lib/sentry/utils/custom_inspection.rb
99
+ - lib/sentry/utils/encoding_helper.rb
98
100
  - lib/sentry/utils/exception_cause_chain.rb
99
101
  - lib/sentry/utils/logging_helper.rb
100
102
  - lib/sentry/utils/real_ip.rb