sentry-ruby 5.4.2 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 310e7ac901b3da90fb400710f95be183627b1e35587f059810669469deb27763
4
- data.tar.gz: 75cff84d0f7238ac444b5f6ce0a67f0150daefa38d9bac9e2b083a6f98ecff6e
3
+ metadata.gz: 292dc37d7e5703be7d5d2e33a84f02b492759f0c8ef656e3ae660a7196301fe4
4
+ data.tar.gz: 1b76f778b06e4928eb32cabe31fc208a4aa92bf8b62f447abf9dd204c97e26cb
5
5
  SHA512:
6
- metadata.gz: 76251bc9b7499bc40b30c1e20c3b638a3cc5bf0a3c5df9f5210fa2b2293a3ee7e6c8c7a1d3073b1055c8788855531c36ece0517ea38e203367c0c8bf6842e130
7
- data.tar.gz: cc3b7a551b8d8c798c5843b895710244857bcf110e57cc4dd1de345e7fe6e09201bf3adf3e4645d9101df201cab8ce4cda0b547b88d137ef1faab8e7a6f1693f
6
+ metadata.gz: ae4dbbb4e60e1dbad0e0e0e5a1fedad72e383b0709453ab4f9f6dc8cc367625e3663848885b893f28f47a3758aed7aec68fc3aa32de3974fdc9e807aa0f5d4bf
7
+ data.tar.gz: 2194f3cb7e4d604d8ddeaf3761406133a049a5e5870ac44e54e8d7ca90d4a41e489b39681b53c912d97c4710bd1813716863364f9de6f233e345c353b31d133f
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'
@@ -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)
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
@@ -92,6 +92,24 @@ module Sentry
92
92
  transaction
93
93
  end
94
94
 
95
+ def with_child_span(**attributes, &block)
96
+ current_span = current_scope.get_span
97
+ return yield(nil) unless current_span
98
+
99
+ result = nil
100
+
101
+ begin
102
+ current_span.with_child_span(**attributes) do |child_span|
103
+ current_scope.set_span(child_span)
104
+ result = yield(child_span)
105
+ end
106
+ ensure
107
+ current_scope.set_span(current_span)
108
+ end
109
+
110
+ result
111
+ end
112
+
95
113
  def capture_exception(exception, **options, &block)
96
114
  check_argument_type!(exception, ::Exception)
97
115
 
@@ -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,9 +60,10 @@ 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,
@@ -79,6 +80,7 @@ module Sentry
79
80
  @start_timestamp = start_timestamp || Sentry.utc_now.to_f
80
81
  @timestamp = timestamp
81
82
  @description = description
83
+ @transaction = transaction
82
84
  @op = op
83
85
  @status = status
84
86
  @data = {}
@@ -104,6 +106,13 @@ module Sentry
104
106
  "#{@trace_id}-#{@span_id}-#{sampled_flag}"
105
107
  end
106
108
 
109
+ # Generates a W3C Baggage header string for distributed tracing
110
+ # from the incoming baggage stored on the transaction.
111
+ # @return [String, nil]
112
+ def to_baggage
113
+ transaction.get_baggage&.serialize
114
+ end
115
+
107
116
  # @return [Hash]
108
117
  def to_hash
109
118
  {
@@ -136,9 +145,8 @@ module Sentry
136
145
  # Starts a child span with given attributes.
137
146
  # @param attributes [Hash] the attributes for the child span.
138
147
  def start_child(**attributes)
139
- attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
148
+ attributes = attributes.dup.merge(transaction: @transaction, trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
140
149
  new_span = Span.new(**attributes)
141
- new_span.transaction = transaction
142
150
  new_span.span_recorder = span_recorder
143
151
 
144
152
  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,34 @@ 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
52
+
53
+ def initialize(
54
+ hub:,
55
+ name: nil,
56
+ source: :custom,
57
+ parent_sampled: nil,
58
+ baggage: nil,
59
+ **options
60
+ )
61
+ super(transaction: self, **options)
36
62
 
37
63
  @name = name
64
+ @source = SOURCES.include?(source) ? source.to_sym : :custom
38
65
  @parent_sampled = parent_sampled
39
- @transaction = self
40
66
  @hub = hub
67
+ @baggage = baggage
41
68
  @configuration = hub.configuration # to be removed
42
69
  @tracing_enabled = hub.configuration.tracing_enabled?
43
70
  @traces_sampler = hub.configuration.traces_sampler
44
71
  @traces_sample_rate = hub.configuration.traces_sample_rate
45
72
  @logger = hub.configuration.logger
73
+ @release = hub.configuration.release
74
+ @environment = hub.configuration.environment
75
+ @dsn = hub.configuration.dsn
76
+ @effective_sample_rate = nil
46
77
  init_span_recorder
47
78
  end
48
79
 
@@ -52,10 +83,11 @@ module Sentry
52
83
  #
53
84
  # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
54
85
  # @param sentry_trace [String] the trace string from the previous transaction.
86
+ # @param baggage [String, nil] the incoming baggage header string.
55
87
  # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
56
88
  # @param options [Hash] the options you want to use to initialize a Transaction instance.
57
89
  # @return [Transaction, nil]
58
- def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
90
+ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
59
91
  return unless hub.configuration.tracing_enabled?
60
92
  return unless sentry_trace
61
93
 
@@ -70,13 +102,38 @@ module Sentry
70
102
  sampled_flag != "0"
71
103
  end
72
104
 
73
- new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
105
+ baggage = if baggage && !baggage.empty?
106
+ Baggage.from_incoming_header(baggage)
107
+ else
108
+ # If there's an incoming sentry-trace but no incoming baggage header,
109
+ # for instance in traces coming from older SDKs,
110
+ # baggage will be empty and frozen and won't be populated as head SDK.
111
+ Baggage.new({})
112
+ end
113
+
114
+ baggage.freeze!
115
+
116
+ new(
117
+ trace_id: trace_id,
118
+ parent_span_id: parent_span_id,
119
+ parent_sampled: parent_sampled,
120
+ hub: hub,
121
+ baggage: baggage,
122
+ **options
123
+ )
74
124
  end
75
125
 
76
126
  # @return [Hash]
77
127
  def to_hash
78
128
  hash = super
79
- hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
129
+
130
+ hash.merge!(
131
+ name: @name,
132
+ source: @source,
133
+ sampled: @sampled,
134
+ parent_sampled: @parent_sampled
135
+ )
136
+
80
137
  hash
81
138
  end
82
139
 
@@ -103,7 +160,10 @@ module Sentry
103
160
  return
104
161
  end
105
162
 
106
- return unless @sampled.nil?
163
+ unless @sampled.nil?
164
+ @effective_sample_rate = @sampled ? 1.0 : 0.0
165
+ return
166
+ end
107
167
 
108
168
  sample_rate =
109
169
  if @traces_sampler.is_a?(Proc)
@@ -116,7 +176,11 @@ module Sentry
116
176
 
117
177
  transaction_description = generate_transaction_description
118
178
 
119
- unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
179
+ if [true, false].include?(sample_rate)
180
+ @effective_sample_rate = sample_rate ? 1.0 : 0.0
181
+ elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
182
+ @effective_sample_rate = sample_rate.to_f
183
+ else
120
184
  @sampled = false
121
185
  log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
122
186
  return
@@ -172,6 +236,14 @@ module Sentry
172
236
  end
173
237
  end
174
238
 
239
+ # Get the existing frozen incoming baggage
240
+ # or populate one with sentry- items as the head SDK.
241
+ # @return [Baggage]
242
+ def get_baggage
243
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
244
+ @baggage
245
+ end
246
+
175
247
  protected
176
248
 
177
249
  def init_span_recorder(limit = 1000)
@@ -188,6 +260,29 @@ module Sentry
188
260
  result
189
261
  end
190
262
 
263
+ def populate_head_baggage
264
+ items = {
265
+ "trace_id" => trace_id,
266
+ "sample_rate" => effective_sample_rate&.to_s,
267
+ "environment" => @environment,
268
+ "release" => @release,
269
+ "public_key" => @dsn&.public_key
270
+ }
271
+
272
+ items["transaction"] = name unless source_low_quality?
273
+
274
+ user = @hub.current_scope&.user
275
+ items["user_segment"] = user["segment"] if user && user["segment"]
276
+
277
+ items.compact!
278
+ @baggage = Baggage.new(items, mutable: false)
279
+ end
280
+
281
+ # These are high cardinality and thus bad
282
+ def source_low_quality?
283
+ source == :url
284
+ end
285
+
191
286
  class SpanRecorder
192
287
  attr_reader :max_length, :spans
193
288
 
@@ -8,9 +8,27 @@ 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!(trace: transaction.get_trace_context)
23
+ self.timestamp = transaction.timestamp
24
+ self.start_timestamp = transaction.start_timestamp
25
+ self.tags = transaction.tags
26
+ self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
27
+
28
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
29
+ self.spans = finished_spans.map(&:to_hash)
30
+ end
31
+
14
32
  # Sets the event's start_timestamp.
15
33
  # @param time [Time, Float]
16
34
  # @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.6.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.6.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-11-08 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