sentry-ruby 5.3.1 → 5.16.1

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