sentry-ruby 5.4.2 → 5.9.0

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