sentry-ruby 5.4.2 → 5.6.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: 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