sentry-ruby 5.4.2 → 5.9.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.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sentry/baggage"
4
+ require "sentry/profiler"
5
+
3
6
  module Sentry
4
7
  class Transaction < Span
5
8
  SENTRY_TRACE_REGEXP = Regexp.new(
@@ -12,16 +15,33 @@ module Sentry
12
15
  UNLABELD_NAME = "<unlabeled transaction>".freeze
13
16
  MESSAGE_PREFIX = "[Tracing]"
14
17
 
18
+ # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
19
+ SOURCES = %i(custom url route view component task)
20
+
15
21
  include LoggingHelper
16
22
 
17
23
  # The name of the transaction.
18
24
  # @return [String]
19
25
  attr_reader :name
20
26
 
27
+ # The source of the transaction name.
28
+ # @return [Symbol]
29
+ attr_reader :source
30
+
21
31
  # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
22
32
  # @return [String]
23
33
  attr_reader :parent_sampled
24
34
 
35
+ # The parsed incoming W3C baggage header.
36
+ # This is only for accessing the current baggage variable.
37
+ # Please use the #get_baggage method for interfacing outside this class.
38
+ # @return [Baggage, nil]
39
+ attr_reader :baggage
40
+
41
+ # The measurements added to the transaction.
42
+ # @return [Hash]
43
+ attr_reader :measurements
44
+
25
45
  # @deprecated Use Sentry.get_current_hub instead.
26
46
  attr_reader :hub
27
47
 
@@ -31,18 +51,44 @@ module Sentry
31
51
  # @deprecated Use Sentry.logger instead.
32
52
  attr_reader :logger
33
53
 
34
- def initialize(name: nil, parent_sampled: nil, hub:, **options)
35
- super(**options)
54
+ # The effective sample rate at which this transaction was sampled.
55
+ # @return [Float, nil]
56
+ attr_reader :effective_sample_rate
36
57
 
37
- @name = name
58
+ # Additional contexts stored directly on the transaction object.
59
+ # @return [Hash]
60
+ attr_reader :contexts
61
+
62
+ # The Profiler instance for this transaction.
63
+ # @return [Profiler]
64
+ attr_reader :profiler
65
+
66
+ def initialize(
67
+ hub:,
68
+ name: nil,
69
+ source: :custom,
70
+ parent_sampled: nil,
71
+ baggage: nil,
72
+ **options
73
+ )
74
+ super(transaction: self, **options)
75
+
76
+ set_name(name, source: source)
38
77
  @parent_sampled = parent_sampled
39
- @transaction = self
40
78
  @hub = hub
79
+ @baggage = baggage
41
80
  @configuration = hub.configuration # to be removed
42
81
  @tracing_enabled = hub.configuration.tracing_enabled?
43
82
  @traces_sampler = hub.configuration.traces_sampler
44
83
  @traces_sample_rate = hub.configuration.traces_sample_rate
45
84
  @logger = hub.configuration.logger
85
+ @release = hub.configuration.release
86
+ @environment = hub.configuration.environment
87
+ @dsn = hub.configuration.dsn
88
+ @effective_sample_rate = nil
89
+ @contexts = {}
90
+ @measurements = {}
91
+ @profiler = Profiler.new(@configuration)
46
92
  init_span_recorder
47
93
  end
48
94
 
@@ -52,31 +98,65 @@ module Sentry
52
98
  #
53
99
  # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
54
100
  # @param sentry_trace [String] the trace string from the previous transaction.
101
+ # @param baggage [String, nil] the incoming baggage header string.
55
102
  # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
56
103
  # @param options [Hash] the options you want to use to initialize a Transaction instance.
57
104
  # @return [Transaction, nil]
58
- def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
105
+ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
59
106
  return unless hub.configuration.tracing_enabled?
60
107
  return unless sentry_trace
61
108
 
109
+ sentry_trace_data = extract_sentry_trace(sentry_trace)
110
+ return unless sentry_trace_data
111
+
112
+ trace_id, parent_span_id, parent_sampled = sentry_trace_data
113
+
114
+ baggage = if baggage && !baggage.empty?
115
+ Baggage.from_incoming_header(baggage)
116
+ else
117
+ # If there's an incoming sentry-trace but no incoming baggage header,
118
+ # for instance in traces coming from older SDKs,
119
+ # baggage will be empty and frozen and won't be populated as head SDK.
120
+ Baggage.new({})
121
+ end
122
+
123
+ baggage.freeze!
124
+
125
+ new(
126
+ trace_id: trace_id,
127
+ parent_span_id: parent_span_id,
128
+ parent_sampled: parent_sampled,
129
+ hub: hub,
130
+ baggage: baggage,
131
+ **options
132
+ )
133
+ end
134
+
135
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
136
+ #
137
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
138
+ # @return [Array, nil]
139
+ def self.extract_sentry_trace(sentry_trace)
62
140
  match = SENTRY_TRACE_REGEXP.match(sentry_trace)
63
- return if match.nil?
64
- trace_id, parent_span_id, sampled_flag = match[1..3]
141
+ return nil if match.nil?
65
142
 
66
- parent_sampled =
67
- if sampled_flag.nil?
68
- nil
69
- else
70
- sampled_flag != "0"
71
- end
143
+ trace_id, parent_span_id, sampled_flag = match[1..3]
144
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
72
145
 
73
- new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
146
+ [trace_id, parent_span_id, parent_sampled]
74
147
  end
75
148
 
76
149
  # @return [Hash]
77
150
  def to_hash
78
151
  hash = super
79
- hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
152
+
153
+ hash.merge!(
154
+ name: @name,
155
+ source: @source,
156
+ sampled: @sampled,
157
+ parent_sampled: @parent_sampled
158
+ )
159
+
80
160
  hash
81
161
  end
82
162
 
@@ -94,6 +174,15 @@ module Sentry
94
174
  copy
95
175
  end
96
176
 
177
+ # Sets a custom measurement on the transaction.
178
+ # @param name [String] name of the measurement
179
+ # @param value [Float] value of the measurement
180
+ # @param unit [String] unit of the measurement
181
+ # @return [void]
182
+ def set_measurement(name, value, unit = "")
183
+ @measurements[name] = { value: value, unit: unit }
184
+ end
185
+
97
186
  # Sets initial sampling decision of the transaction.
98
187
  # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
99
188
  # @return [void]
@@ -103,7 +192,10 @@ module Sentry
103
192
  return
104
193
  end
105
194
 
106
- return unless @sampled.nil?
195
+ unless @sampled.nil?
196
+ @effective_sample_rate = @sampled ? 1.0 : 0.0
197
+ return
198
+ end
107
199
 
108
200
  sample_rate =
109
201
  if @traces_sampler.is_a?(Proc)
@@ -116,7 +208,11 @@ module Sentry
116
208
 
117
209
  transaction_description = generate_transaction_description
118
210
 
119
- unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
211
+ if [true, false].include?(sample_rate)
212
+ @effective_sample_rate = sample_rate ? 1.0 : 0.0
213
+ elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
214
+ @effective_sample_rate = sample_rate.to_f
215
+ else
120
216
  @sampled = false
121
217
  log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
122
218
  return
@@ -146,7 +242,7 @@ module Sentry
146
242
  # Finishes the transaction's recording and send it to Sentry.
147
243
  # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
148
244
  # @return [TransactionEvent]
149
- def finish(hub: nil)
245
+ def finish(hub: nil, end_timestamp: nil)
150
246
  if hub
151
247
  log_warn(
152
248
  <<~MSG
@@ -158,12 +254,14 @@ module Sentry
158
254
 
159
255
  hub ||= @hub
160
256
 
161
- super() # Span#finish doesn't take arguments
257
+ super(end_timestamp: end_timestamp)
162
258
 
163
259
  if @name.nil?
164
260
  @name = UNLABELD_NAME
165
261
  end
166
262
 
263
+ @profiler.stop
264
+
167
265
  if @sampled
168
266
  event = hub.current_client.event_from_transaction(self)
169
267
  hub.capture_event(event)
@@ -172,6 +270,39 @@ module Sentry
172
270
  end
173
271
  end
174
272
 
273
+ # Get the existing frozen incoming baggage
274
+ # or populate one with sentry- items as the head SDK.
275
+ # @return [Baggage]
276
+ def get_baggage
277
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
278
+ @baggage
279
+ end
280
+
281
+ # Set the transaction name directly.
282
+ # Considered internal api since it bypasses the usual scope logic.
283
+ # @param name [String]
284
+ # @param source [Symbol]
285
+ # @return [void]
286
+ def set_name(name, source: :custom)
287
+ @name = name
288
+ @source = SOURCES.include?(source) ? source.to_sym : :custom
289
+ end
290
+
291
+ # Set contexts directly on the transaction.
292
+ # @param key [String, Symbol]
293
+ # @param value [Object]
294
+ # @return [void]
295
+ def set_context(key, value)
296
+ @contexts[key] = value
297
+ end
298
+
299
+ # Start the profiler.
300
+ # @return [void]
301
+ def start_profiler!
302
+ profiler.set_initial_sample_decision(sampled)
303
+ profiler.start
304
+ end
305
+
175
306
  protected
176
307
 
177
308
  def init_span_recorder(limit = 1000)
@@ -188,6 +319,29 @@ module Sentry
188
319
  result
189
320
  end
190
321
 
322
+ def populate_head_baggage
323
+ items = {
324
+ "trace_id" => trace_id,
325
+ "sample_rate" => effective_sample_rate&.to_s,
326
+ "environment" => @environment,
327
+ "release" => @release,
328
+ "public_key" => @dsn&.public_key
329
+ }
330
+
331
+ items["transaction"] = name unless source_low_quality?
332
+
333
+ user = @hub.current_scope&.user
334
+ items["user_segment"] = user["segment"] if user && user["segment"]
335
+
336
+ items.compact!
337
+ @baggage = Baggage.new(items, mutable: false)
338
+ end
339
+
340
+ # These are high cardinality and thus bad
341
+ def source_low_quality?
342
+ source == :url
343
+ end
344
+
191
345
  class SpanRecorder
192
346
  attr_reader :max_length, :spans
193
347
 
@@ -8,9 +8,37 @@ 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
+
14
+ # @return [Hash]
15
+ attr_accessor :measurements
16
+
11
17
  # @return [Float, nil]
12
18
  attr_reader :start_timestamp
13
19
 
20
+ # @return [Hash, nil]
21
+ attr_accessor :profile
22
+
23
+ def initialize(transaction:, **options)
24
+ super(**options)
25
+
26
+ self.transaction = transaction.name
27
+ self.transaction_info = { source: transaction.source }
28
+ self.contexts.merge!(transaction.contexts)
29
+ self.contexts.merge!(trace: transaction.get_trace_context)
30
+ self.timestamp = transaction.timestamp
31
+ self.start_timestamp = transaction.start_timestamp
32
+ self.tags = transaction.tags
33
+ self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
34
+ self.measurements = transaction.measurements
35
+
36
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
37
+ self.spans = finished_spans.map(&:to_hash)
38
+
39
+ populate_profile(transaction)
40
+ end
41
+
14
42
  # Sets the event's start_timestamp.
15
43
  # @param time [Time, Float]
16
44
  # @return [void]
@@ -23,7 +51,33 @@ module Sentry
23
51
  data = super
24
52
  data[:spans] = @spans.map(&:to_hash) if @spans
25
53
  data[:start_timestamp] = @start_timestamp
54
+ data[:measurements] = @measurements
26
55
  data
27
56
  end
57
+
58
+ private
59
+
60
+ def populate_profile(transaction)
61
+ profile_hash = transaction.profiler.to_hash
62
+ return if profile_hash.empty?
63
+
64
+ profile_hash.merge!(
65
+ environment: environment,
66
+ release: release,
67
+ timestamp: Time.at(start_timestamp).iso8601,
68
+ device: { architecture: Scope.os_context[:machine] },
69
+ os: { name: Scope.os_context[:name], version: Scope.os_context[:version] },
70
+ runtime: Scope.runtime_context,
71
+ transaction: {
72
+ id: event_id,
73
+ name: transaction.name,
74
+ trace_id: transaction.trace_id,
75
+ # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
76
+ active_thead_id: '0'
77
+ }
78
+ )
79
+
80
+ self.profile = profile_hash
81
+ end
28
82
  end
29
83
  end
@@ -136,20 +136,31 @@ 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' },
150
154
  event_payload
151
155
  )
152
156
 
157
+ if event.is_a?(TransactionEvent) && event.profile
158
+ envelope.add_item(
159
+ { type: 'profile', content_type: 'application/json' },
160
+ event.profile
161
+ )
162
+ end
163
+
153
164
  client_report_headers, client_report_payload = fetch_pending_client_report
154
165
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
155
166
 
@@ -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.9.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
@@ -70,8 +73,18 @@ module Sentry
70
73
  ##### Patch Registration #####
71
74
 
72
75
  # @!visibility private
73
- def register_patch(&block)
74
- registered_patches << block
76
+ def register_patch(patch = nil, target = nil, &block)
77
+ if patch && block
78
+ raise ArgumentError.new("Please provide either a patch and its target OR a block, but not both")
79
+ end
80
+
81
+ if block
82
+ registered_patches << block
83
+ else
84
+ registered_patches << proc do
85
+ target.send(:prepend, patch) unless target.ancestors.include?(patch)
86
+ end
87
+ end
75
88
  end
76
89
 
77
90
  # @!visibility private
@@ -209,7 +222,7 @@ module Sentry
209
222
  nil
210
223
  end
211
224
 
212
- if config.capture_exception_frame_locals
225
+ if config.include_local_variables
213
226
  exception_locals_tp.enable
214
227
  end
215
228
 
@@ -231,7 +244,7 @@ module Sentry
231
244
  @session_flusher = nil
232
245
  end
233
246
 
234
- if configuration&.capture_exception_frame_locals
247
+ if configuration&.include_local_variables
235
248
  exception_locals_tp.disable
236
249
  end
237
250
 
@@ -348,7 +361,7 @@ module Sentry
348
361
  # @yieldparam scope [Scope]
349
362
  # @return [void]
350
363
  def with_scope(&block)
351
- return unless initialized?
364
+ return yield unless initialized?
352
365
  get_current_hub.with_scope(&block)
353
366
  end
354
367
 
@@ -439,22 +452,8 @@ module Sentry
439
452
  # end
440
453
  #
441
454
  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
455
+ return yield(nil) unless Sentry.initialized?
456
+ get_current_hub.with_child_span(**attributes, &block)
458
457
  end
459
458
 
460
459
  # Returns the id of the lastly reported Sentry::Event.
@@ -473,6 +472,23 @@ module Sentry
473
472
  !!exc.instance_variable_get(CAPTURED_SIGNATURE)
474
473
  end
475
474
 
475
+ # Add a global event processor [Proc].
476
+ # These run before scope event processors.
477
+ #
478
+ # @yieldparam event [Event]
479
+ # @yieldparam hint [Hash, nil]
480
+ # @return [void]
481
+ #
482
+ # @example
483
+ # Sentry.add_global_event_processor do |event, hint|
484
+ # event.tags = { foo: 42 }
485
+ # event
486
+ # end
487
+ #
488
+ def add_global_event_processor(&block)
489
+ Scope.add_global_event_processor(&block)
490
+ end
491
+
476
492
  ##### Helpers #####
477
493
 
478
494
  # @!visibility private
@@ -503,3 +519,4 @@ end
503
519
  # patches
504
520
  require "sentry/net/http"
505
521
  require "sentry/redis"
522
+ require "sentry/puma"
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.9.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: 2023-04-19 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
@@ -77,6 +78,8 @@ files:
77
78
  - lib/sentry/linecache.rb
78
79
  - lib/sentry/logger.rb
79
80
  - lib/sentry/net/http.rb
81
+ - lib/sentry/profiler.rb
82
+ - lib/sentry/puma.rb
80
83
  - lib/sentry/rack.rb
81
84
  - lib/sentry/rack/capture_exceptions.rb
82
85
  - lib/sentry/rake.rb
@@ -95,6 +98,7 @@ files:
95
98
  - lib/sentry/transport/http_transport.rb
96
99
  - lib/sentry/utils/argument_checking_helper.rb
97
100
  - lib/sentry/utils/custom_inspection.rb
101
+ - lib/sentry/utils/encoding_helper.rb
98
102
  - lib/sentry/utils/exception_cause_chain.rb
99
103
  - lib/sentry/utils/logging_helper.rb
100
104
  - lib/sentry/utils/real_ip.rb