sentry-ruby 5.3.1 → 5.16.1

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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/Gemfile +26 -0
  7. data/Makefile +4 -0
  8. data/README.md +11 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +79 -0
  13. data/lib/sentry/backpressure_monitor.rb +75 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +70 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +76 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/check_in_event.rb +60 -0
  20. data/lib/sentry/client.rb +248 -0
  21. data/lib/sentry/configuration.rb +650 -0
  22. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  23. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  24. data/lib/sentry/cron/configuration.rb +23 -0
  25. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  26. data/lib/sentry/cron/monitor_config.rb +53 -0
  27. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  28. data/lib/sentry/dsn.rb +53 -0
  29. data/lib/sentry/envelope.rb +93 -0
  30. data/lib/sentry/error_event.rb +38 -0
  31. data/lib/sentry/event.rb +156 -0
  32. data/lib/sentry/exceptions.rb +9 -0
  33. data/lib/sentry/hub.rb +316 -0
  34. data/lib/sentry/integrable.rb +32 -0
  35. data/lib/sentry/interface.rb +16 -0
  36. data/lib/sentry/interfaces/exception.rb +43 -0
  37. data/lib/sentry/interfaces/request.rb +134 -0
  38. data/lib/sentry/interfaces/single_exception.rb +67 -0
  39. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  40. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  41. data/lib/sentry/interfaces/threads.rb +42 -0
  42. data/lib/sentry/linecache.rb +47 -0
  43. data/lib/sentry/logger.rb +20 -0
  44. data/lib/sentry/net/http.rb +106 -0
  45. data/lib/sentry/profiler.rb +233 -0
  46. data/lib/sentry/propagation_context.rb +134 -0
  47. data/lib/sentry/puma.rb +32 -0
  48. data/lib/sentry/rack/capture_exceptions.rb +79 -0
  49. data/lib/sentry/rack.rb +5 -0
  50. data/lib/sentry/rake.rb +28 -0
  51. data/lib/sentry/redis.rb +108 -0
  52. data/lib/sentry/release_detector.rb +39 -0
  53. data/lib/sentry/scope.rb +360 -0
  54. data/lib/sentry/session.rb +33 -0
  55. data/lib/sentry/session_flusher.rb +90 -0
  56. data/lib/sentry/span.rb +273 -0
  57. data/lib/sentry/test_helper.rb +84 -0
  58. data/lib/sentry/transaction.rb +359 -0
  59. data/lib/sentry/transaction_event.rb +80 -0
  60. data/lib/sentry/transport/configuration.rb +98 -0
  61. data/lib/sentry/transport/dummy_transport.rb +21 -0
  62. data/lib/sentry/transport/http_transport.rb +206 -0
  63. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  64. data/lib/sentry/transport.rb +225 -0
  65. data/lib/sentry/utils/argument_checking_helper.rb +19 -0
  66. data/lib/sentry/utils/custom_inspection.rb +14 -0
  67. data/lib/sentry/utils/encoding_helper.rb +22 -0
  68. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  69. data/lib/sentry/utils/logging_helper.rb +26 -0
  70. data/lib/sentry/utils/real_ip.rb +84 -0
  71. data/lib/sentry/utils/request_id.rb +18 -0
  72. data/lib/sentry/version.rb +5 -0
  73. data/lib/sentry-ruby.rb +580 -0
  74. data/sentry-ruby-core.gemspec +23 -0
  75. data/sentry-ruby.gemspec +24 -0
  76. metadata +75 -16
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/baggage"
4
+ require "sentry/profiler"
5
+ require "sentry/propagation_context"
6
+
7
+ module Sentry
8
+ class Transaction < Span
9
+ # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
10
+ SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
11
+
12
+ UNLABELD_NAME = "<unlabeled transaction>".freeze
13
+ MESSAGE_PREFIX = "[Tracing]"
14
+
15
+ # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
16
+ SOURCES = %i(custom url route view component task)
17
+
18
+ include LoggingHelper
19
+
20
+ # The name of the transaction.
21
+ # @return [String]
22
+ attr_reader :name
23
+
24
+ # The source of the transaction name.
25
+ # @return [Symbol]
26
+ attr_reader :source
27
+
28
+ # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
29
+ # @return [String]
30
+ attr_reader :parent_sampled
31
+
32
+ # The parsed incoming W3C baggage header.
33
+ # This is only for accessing the current baggage variable.
34
+ # Please use the #get_baggage method for interfacing outside this class.
35
+ # @return [Baggage, nil]
36
+ attr_reader :baggage
37
+
38
+ # The measurements added to the transaction.
39
+ # @return [Hash]
40
+ attr_reader :measurements
41
+
42
+ # @deprecated Use Sentry.get_current_hub instead.
43
+ attr_reader :hub
44
+
45
+ # @deprecated Use Sentry.configuration instead.
46
+ attr_reader :configuration
47
+
48
+ # @deprecated Use Sentry.logger instead.
49
+ attr_reader :logger
50
+
51
+ # The effective sample rate at which this transaction was sampled.
52
+ # @return [Float, nil]
53
+ attr_reader :effective_sample_rate
54
+
55
+ # Additional contexts stored directly on the transaction object.
56
+ # @return [Hash]
57
+ attr_reader :contexts
58
+
59
+ # The Profiler instance for this transaction.
60
+ # @return [Profiler]
61
+ attr_reader :profiler
62
+
63
+ def initialize(
64
+ hub:,
65
+ name: nil,
66
+ source: :custom,
67
+ parent_sampled: nil,
68
+ baggage: nil,
69
+ **options
70
+ )
71
+ super(transaction: self, **options)
72
+
73
+ set_name(name, source: source)
74
+ @parent_sampled = parent_sampled
75
+ @hub = hub
76
+ @baggage = baggage
77
+ @configuration = hub.configuration # to be removed
78
+ @tracing_enabled = hub.configuration.tracing_enabled?
79
+ @traces_sampler = hub.configuration.traces_sampler
80
+ @traces_sample_rate = hub.configuration.traces_sample_rate
81
+ @logger = hub.configuration.logger
82
+ @release = hub.configuration.release
83
+ @environment = hub.configuration.environment
84
+ @dsn = hub.configuration.dsn
85
+ @effective_sample_rate = nil
86
+ @contexts = {}
87
+ @measurements = {}
88
+ @profiler = Profiler.new(@configuration)
89
+ init_span_recorder
90
+ end
91
+
92
+ # @deprecated use Sentry.continue_trace instead.
93
+ #
94
+ # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
95
+ #
96
+ # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
97
+ #
98
+ # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
99
+ # @param sentry_trace [String] the trace string from the previous transaction.
100
+ # @param baggage [String, nil] the incoming baggage header string.
101
+ # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
102
+ # @param options [Hash] the options you want to use to initialize a Transaction instance.
103
+ # @return [Transaction, nil]
104
+ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
105
+ return unless hub.configuration.tracing_enabled?
106
+ return unless sentry_trace
107
+
108
+ sentry_trace_data = extract_sentry_trace(sentry_trace)
109
+ return unless sentry_trace_data
110
+
111
+ trace_id, parent_span_id, parent_sampled = sentry_trace_data
112
+
113
+ baggage = if baggage && !baggage.empty?
114
+ Baggage.from_incoming_header(baggage)
115
+ else
116
+ # If there's an incoming sentry-trace but no incoming baggage header,
117
+ # for instance in traces coming from older SDKs,
118
+ # baggage will be empty and frozen and won't be populated as head SDK.
119
+ Baggage.new({})
120
+ end
121
+
122
+ baggage.freeze!
123
+
124
+ new(
125
+ trace_id: trace_id,
126
+ parent_span_id: parent_span_id,
127
+ parent_sampled: parent_sampled,
128
+ hub: hub,
129
+ baggage: baggage,
130
+ **options
131
+ )
132
+ end
133
+
134
+ # @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead.
135
+ # @return [Array, nil]
136
+ def self.extract_sentry_trace(sentry_trace)
137
+ PropagationContext.extract_sentry_trace(sentry_trace)
138
+ end
139
+
140
+ # @return [Hash]
141
+ def to_hash
142
+ hash = super
143
+
144
+ hash.merge!(
145
+ name: @name,
146
+ source: @source,
147
+ sampled: @sampled,
148
+ parent_sampled: @parent_sampled
149
+ )
150
+
151
+ hash
152
+ end
153
+
154
+ # @return [Transaction]
155
+ def deep_dup
156
+ copy = super
157
+ copy.init_span_recorder(@span_recorder.max_length)
158
+
159
+ @span_recorder.spans.each do |span|
160
+ # span_recorder's first span is the current span, which should not be added to the copy's spans
161
+ next if span == self
162
+ copy.span_recorder.add(span.dup)
163
+ end
164
+
165
+ copy
166
+ end
167
+
168
+ # Sets a custom measurement on the transaction.
169
+ # @param name [String] name of the measurement
170
+ # @param value [Float] value of the measurement
171
+ # @param unit [String] unit of the measurement
172
+ # @return [void]
173
+ def set_measurement(name, value, unit = "")
174
+ @measurements[name] = { value: value, unit: unit }
175
+ end
176
+
177
+ # Sets initial sampling decision of the transaction.
178
+ # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
179
+ # @return [void]
180
+ def set_initial_sample_decision(sampling_context:)
181
+ unless @tracing_enabled
182
+ @sampled = false
183
+ return
184
+ end
185
+
186
+ unless @sampled.nil?
187
+ @effective_sample_rate = @sampled ? 1.0 : 0.0
188
+ return
189
+ end
190
+
191
+ sample_rate =
192
+ if @traces_sampler.is_a?(Proc)
193
+ @traces_sampler.call(sampling_context)
194
+ elsif !sampling_context[:parent_sampled].nil?
195
+ sampling_context[:parent_sampled]
196
+ else
197
+ @traces_sample_rate
198
+ end
199
+
200
+ transaction_description = generate_transaction_description
201
+
202
+ if [true, false].include?(sample_rate)
203
+ @effective_sample_rate = sample_rate ? 1.0 : 0.0
204
+ elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
205
+ @effective_sample_rate = sample_rate.to_f
206
+ else
207
+ @sampled = false
208
+ log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
209
+ return
210
+ end
211
+
212
+ if sample_rate == 0.0 || sample_rate == false
213
+ @sampled = false
214
+ log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
215
+ return
216
+ end
217
+
218
+ if sample_rate == true
219
+ @sampled = true
220
+ else
221
+ if Sentry.backpressure_monitor
222
+ factor = Sentry.backpressure_monitor.downsample_factor
223
+ @effective_sample_rate /= 2**factor
224
+ end
225
+
226
+ @sampled = Random.rand < @effective_sample_rate
227
+ end
228
+
229
+ if @sampled
230
+ log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
231
+ else
232
+ log_debug(
233
+ "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
234
+ )
235
+ end
236
+ end
237
+
238
+ # Finishes the transaction's recording and send it to Sentry.
239
+ # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
240
+ # @return [TransactionEvent]
241
+ def finish(hub: nil, end_timestamp: nil)
242
+ if hub
243
+ log_warn(
244
+ <<~MSG
245
+ Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
246
+ Please use `Hub#start_transaction` with the designated hub.
247
+ MSG
248
+ )
249
+ end
250
+
251
+ hub ||= @hub
252
+
253
+ super(end_timestamp: end_timestamp)
254
+
255
+ if @name.nil?
256
+ @name = UNLABELD_NAME
257
+ end
258
+
259
+ @profiler.stop
260
+
261
+ if @sampled
262
+ event = hub.current_client.event_from_transaction(self)
263
+ hub.capture_event(event)
264
+ else
265
+ is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive?
266
+ reason = is_backpressure ? :backpressure : :sample_rate
267
+ hub.current_client.transport.record_lost_event(reason, 'transaction')
268
+ end
269
+ end
270
+
271
+ # Get the existing frozen incoming baggage
272
+ # or populate one with sentry- items as the head SDK.
273
+ # @return [Baggage]
274
+ def get_baggage
275
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
276
+ @baggage
277
+ end
278
+
279
+ # Set the transaction name directly.
280
+ # Considered internal api since it bypasses the usual scope logic.
281
+ # @param name [String]
282
+ # @param source [Symbol]
283
+ # @return [void]
284
+ def set_name(name, source: :custom)
285
+ @name = name
286
+ @source = SOURCES.include?(source) ? source.to_sym : :custom
287
+ end
288
+
289
+ # Set contexts directly on the transaction.
290
+ # @param key [String, Symbol]
291
+ # @param value [Object]
292
+ # @return [void]
293
+ def set_context(key, value)
294
+ @contexts[key] = value
295
+ end
296
+
297
+ # Start the profiler.
298
+ # @return [void]
299
+ def start_profiler!
300
+ profiler.set_initial_sample_decision(sampled)
301
+ profiler.start
302
+ end
303
+
304
+ protected
305
+
306
+ def init_span_recorder(limit = 1000)
307
+ @span_recorder = SpanRecorder.new(limit)
308
+ @span_recorder.add(self)
309
+ end
310
+
311
+ private
312
+
313
+ def generate_transaction_description
314
+ result = op.nil? ? "" : "<#{@op}> "
315
+ result += "transaction"
316
+ result += " <#{@name}>" if @name
317
+ result
318
+ end
319
+
320
+ def populate_head_baggage
321
+ items = {
322
+ "trace_id" => trace_id,
323
+ "sample_rate" => effective_sample_rate&.to_s,
324
+ "sampled" => sampled&.to_s,
325
+ "environment" => @environment,
326
+ "release" => @release,
327
+ "public_key" => @dsn&.public_key
328
+ }
329
+
330
+ items["transaction"] = name unless source_low_quality?
331
+
332
+ user = @hub.current_scope&.user
333
+ items["user_segment"] = user["segment"] if user && user["segment"]
334
+
335
+ items.compact!
336
+ @baggage = Baggage.new(items, mutable: false)
337
+ end
338
+
339
+ # These are high cardinality and thus bad
340
+ def source_low_quality?
341
+ source == :url
342
+ end
343
+
344
+ class SpanRecorder
345
+ attr_reader :max_length, :spans
346
+
347
+ def initialize(max_length)
348
+ @max_length = max_length
349
+ @spans = []
350
+ end
351
+
352
+ def add(span)
353
+ if @spans.count < @max_length
354
+ @spans << span
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # TransactionEvent represents events that carry transaction data (type: "transaction").
5
+ class TransactionEvent < Event
6
+ TYPE = "transaction"
7
+
8
+ # @return [<Array[Span]>]
9
+ attr_accessor :spans
10
+
11
+ # @return [Hash]
12
+ attr_accessor :measurements
13
+
14
+ # @return [Float, nil]
15
+ attr_reader :start_timestamp
16
+
17
+ # @return [Hash, nil]
18
+ attr_accessor :profile
19
+
20
+ def initialize(transaction:, **options)
21
+ super(**options)
22
+
23
+ self.transaction = transaction.name
24
+ self.transaction_info = { source: transaction.source }
25
+ self.contexts.merge!(transaction.contexts)
26
+ self.contexts.merge!(trace: transaction.get_trace_context)
27
+ self.timestamp = transaction.timestamp
28
+ self.start_timestamp = transaction.start_timestamp
29
+ self.tags = transaction.tags
30
+ self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
31
+ self.measurements = transaction.measurements
32
+
33
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
34
+ self.spans = finished_spans.map(&:to_hash)
35
+
36
+ populate_profile(transaction)
37
+ end
38
+
39
+ # Sets the event's start_timestamp.
40
+ # @param time [Time, Float]
41
+ # @return [void]
42
+ def start_timestamp=(time)
43
+ @start_timestamp = time.is_a?(Time) ? time.to_f : time
44
+ end
45
+
46
+ # @return [Hash]
47
+ def to_hash
48
+ data = super
49
+ data[:spans] = @spans.map(&:to_hash) if @spans
50
+ data[:start_timestamp] = @start_timestamp
51
+ data[:measurements] = @measurements
52
+ data
53
+ end
54
+
55
+ private
56
+
57
+ def populate_profile(transaction)
58
+ profile_hash = transaction.profiler.to_hash
59
+ return if profile_hash.empty?
60
+
61
+ profile_hash.merge!(
62
+ environment: environment,
63
+ release: release,
64
+ timestamp: Time.at(start_timestamp).iso8601,
65
+ device: { architecture: Scope.os_context[:machine] },
66
+ os: { name: Scope.os_context[:name], version: Scope.os_context[:version] },
67
+ runtime: Scope.runtime_context,
68
+ transaction: {
69
+ id: event_id,
70
+ name: transaction.name,
71
+ trace_id: transaction.trace_id,
72
+ # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
73
+ active_thead_id: '0'
74
+ }
75
+ )
76
+
77
+ self.profile = profile_hash
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Transport
5
+ class Configuration
6
+
7
+ # The timeout in seconds to open a connection to Sentry, in seconds.
8
+ # Default value is 2.
9
+ #
10
+ # @return [Integer]
11
+ attr_accessor :timeout
12
+
13
+ # The timeout in seconds to read data from Sentry, in seconds.
14
+ # Default value is 1.
15
+ #
16
+ # @return [Integer]
17
+ attr_accessor :open_timeout
18
+
19
+ # The proxy configuration to use to connect to Sentry.
20
+ # Accepts either a URI formatted string, URI, or a hash with the `uri`,
21
+ # `user`, and `password` keys.
22
+ #
23
+ # @example
24
+ # # setup proxy using a string:
25
+ # config.transport.proxy = "https://user:password@proxyhost:8080"
26
+ #
27
+ # # setup proxy using a URI:
28
+ # config.transport.proxy = URI("https://user:password@proxyhost:8080")
29
+ #
30
+ # # setup proxy using a hash:
31
+ # config.transport.proxy = {
32
+ # uri: URI("https://proxyhost:8080"),
33
+ # user: "user",
34
+ # password: "password"
35
+ # }
36
+ #
37
+ # If you're using the default transport (`Sentry::HTTPTransport`),
38
+ # proxy settings will also automatically be read from tne environment
39
+ # variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`).
40
+ #
41
+ # @return [String, URI, Hash, nil]
42
+ attr_accessor :proxy
43
+
44
+ # The SSL configuration to use to connect to Sentry.
45
+ # You can either pass a `Hash` containing `ca_file` and `verification` keys,
46
+ # or you can set those options directly on the `Sentry::HTTPTransport::Configuration` object:
47
+ #
48
+ # @example
49
+ # config.transport.ssl = {
50
+ # ca_file: "/path/to/ca_file",
51
+ # verification: true
52
+ # end
53
+ #
54
+ # @return [Hash, nil]
55
+ attr_accessor :ssl
56
+
57
+ # The path to the CA file to use to verify the SSL connection.
58
+ # Default value is `nil`.
59
+ #
60
+ # @return [String, nil]
61
+ attr_accessor :ssl_ca_file
62
+
63
+ # Whether to verify that the peer certificate is valid in SSL connections.
64
+ # Default value is `true`.
65
+ #
66
+ # @return [Boolean]
67
+ attr_accessor :ssl_verification
68
+
69
+ # The encoding to use to compress the request body.
70
+ # Default value is `Sentry::HTTPTransport::GZIP_ENCODING`.
71
+ #
72
+ # @return [String]
73
+ attr_accessor :encoding
74
+
75
+ # The class to use as a transport to connect to Sentry.
76
+ # If this option not set, it will return `nil`, and Sentry will use
77
+ # `Sentry::HTTPTransport` by default.
78
+ #
79
+ # @return [Class, nil]
80
+ attr_reader :transport_class
81
+
82
+ def initialize
83
+ @ssl_verification = true
84
+ @open_timeout = 1
85
+ @timeout = 2
86
+ @encoding = HTTPTransport::GZIP_ENCODING
87
+ end
88
+
89
+ def transport_class=(klass)
90
+ unless klass.is_a?(Class)
91
+ raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}")
92
+ end
93
+
94
+ @transport_class = klass
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class DummyTransport < Transport
5
+ attr_accessor :events, :envelopes
6
+
7
+ def initialize(*)
8
+ super
9
+ @events = []
10
+ @envelopes = []
11
+ end
12
+
13
+ def send_event(event)
14
+ @events << event
15
+ end
16
+
17
+ def send_envelope(envelope)
18
+ @envelopes << envelope
19
+ end
20
+ end
21
+ end