sentry-ruby 5.3.0 → 5.8.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.
Files changed (67) 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/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +31 -0
  8. data/Makefile +4 -0
  9. data/README.md +10 -6
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +81 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +70 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/client.rb +207 -0
  20. data/lib/sentry/configuration.rb +543 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  23. data/lib/sentry/dsn.rb +53 -0
  24. data/lib/sentry/envelope.rb +96 -0
  25. data/lib/sentry/error_event.rb +38 -0
  26. data/lib/sentry/event.rb +178 -0
  27. data/lib/sentry/exceptions.rb +9 -0
  28. data/lib/sentry/hub.rb +241 -0
  29. data/lib/sentry/integrable.rb +26 -0
  30. data/lib/sentry/interface.rb +16 -0
  31. data/lib/sentry/interfaces/exception.rb +43 -0
  32. data/lib/sentry/interfaces/request.rb +134 -0
  33. data/lib/sentry/interfaces/single_exception.rb +65 -0
  34. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  35. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  36. data/lib/sentry/interfaces/threads.rb +42 -0
  37. data/lib/sentry/linecache.rb +47 -0
  38. data/lib/sentry/logger.rb +20 -0
  39. data/lib/sentry/net/http.rb +103 -0
  40. data/lib/sentry/rack/capture_exceptions.rb +82 -0
  41. data/lib/sentry/rack.rb +5 -0
  42. data/lib/sentry/rake.rb +41 -0
  43. data/lib/sentry/redis.rb +107 -0
  44. data/lib/sentry/release_detector.rb +39 -0
  45. data/lib/sentry/scope.rb +339 -0
  46. data/lib/sentry/session.rb +33 -0
  47. data/lib/sentry/session_flusher.rb +90 -0
  48. data/lib/sentry/span.rb +236 -0
  49. data/lib/sentry/test_helper.rb +78 -0
  50. data/lib/sentry/transaction.rb +345 -0
  51. data/lib/sentry/transaction_event.rb +53 -0
  52. data/lib/sentry/transport/configuration.rb +25 -0
  53. data/lib/sentry/transport/dummy_transport.rb +21 -0
  54. data/lib/sentry/transport/http_transport.rb +175 -0
  55. data/lib/sentry/transport.rb +214 -0
  56. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  57. data/lib/sentry/utils/custom_inspection.rb +14 -0
  58. data/lib/sentry/utils/encoding_helper.rb +22 -0
  59. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  60. data/lib/sentry/utils/logging_helper.rb +26 -0
  61. data/lib/sentry/utils/real_ip.rb +84 -0
  62. data/lib/sentry/utils/request_id.rb +18 -0
  63. data/lib/sentry/version.rb +5 -0
  64. data/lib/sentry-ruby.rb +511 -0
  65. data/sentry-ruby-core.gemspec +23 -0
  66. data/sentry-ruby.gemspec +24 -0
  67. metadata +66 -16
@@ -0,0 +1,78 @@
1
+ module Sentry
2
+ module TestHelper
3
+ DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42'
4
+
5
+ # Alters the existing SDK configuration with test-suitable options. Mainly:
6
+ # - Sets a dummy DSN instead of `nil` or an actual DSN.
7
+ # - Sets the transport to DummyTransport, which allows easy access to the captured events.
8
+ # - Disables background worker.
9
+ # - Makes sure the SDK is enabled under the current environment ("test" in most cases).
10
+ #
11
+ # It should be called **before** every test case.
12
+ #
13
+ # @yieldparam config [Configuration]
14
+ # @return [void]
15
+ def setup_sentry_test(&block)
16
+ raise "please make sure the SDK is initialized for testing" unless Sentry.initialized?
17
+ copied_config = Sentry.configuration.dup
18
+ # configure dummy DSN, so the events will not be sent to the actual service
19
+ copied_config.dsn = DUMMY_DSN
20
+ # set transport to DummyTransport, so we can easily intercept the captured events
21
+ copied_config.transport.transport_class = Sentry::DummyTransport
22
+ # make sure SDK allows sending under the current environment
23
+ copied_config.enabled_environments << copied_config.environment unless copied_config.enabled_environments.include?(copied_config.environment)
24
+ # disble async event sending
25
+ copied_config.background_worker_threads = 0
26
+
27
+ # user can overwrite some of the configs, with a few exceptions like:
28
+ # - include_local_variables
29
+ # - auto_session_tracking
30
+ block&.call(copied_config)
31
+
32
+ test_client = Sentry::Client.new(copied_config)
33
+ Sentry.get_current_hub.bind_client(test_client)
34
+ Sentry.get_current_scope.clear
35
+ end
36
+
37
+ # Clears all stored events and envelopes.
38
+ # It should be called **after** every test case.
39
+ # @return [void]
40
+ def teardown_sentry_test
41
+ return unless Sentry.initialized?
42
+
43
+ sentry_transport.events = []
44
+ sentry_transport.envelopes = []
45
+ Sentry.get_current_scope.clear
46
+ end
47
+
48
+ # @return [Transport]
49
+ def sentry_transport
50
+ Sentry.get_current_client.transport
51
+ end
52
+
53
+ # Returns the captured event objects.
54
+ # @return [Array<Event>]
55
+ def sentry_events
56
+ sentry_transport.events
57
+ end
58
+
59
+ # Returns the captured envelope objects.
60
+ # @return [Array<Envelope>]
61
+ def sentry_envelopes
62
+ sentry_transport.envelopes
63
+ end
64
+
65
+ # Returns the last captured event object.
66
+ # @return [Event, nil]
67
+ def last_sentry_event
68
+ sentry_events.last
69
+ end
70
+
71
+ # Extracts SDK's internal exception container (not actual exception objects) from an given event.
72
+ # @return [Array<Sentry::SingleExceptionInterface>]
73
+ def extract_sentry_exceptions(event)
74
+ event&.exception&.values || []
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,345 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/baggage"
4
+
5
+ module Sentry
6
+ class Transaction < Span
7
+ SENTRY_TRACE_REGEXP = Regexp.new(
8
+ "^[ \t]*" + # whitespace
9
+ "([0-9a-f]{32})?" + # trace_id
10
+ "-?([0-9a-f]{16})?" + # span_id
11
+ "-?([01])?" + # sampled
12
+ "[ \t]*$" # whitespace
13
+ )
14
+ UNLABELD_NAME = "<unlabeled transaction>".freeze
15
+ MESSAGE_PREFIX = "[Tracing]"
16
+
17
+ # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
18
+ SOURCES = %i(custom url route view component task)
19
+
20
+ include LoggingHelper
21
+
22
+ # The name of the transaction.
23
+ # @return [String]
24
+ attr_reader :name
25
+
26
+ # The source of the transaction name.
27
+ # @return [Symbol]
28
+ attr_reader :source
29
+
30
+ # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
31
+ # @return [String]
32
+ attr_reader :parent_sampled
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
+
40
+ # The measurements added to the transaction.
41
+ # @return [Hash]
42
+ attr_reader :measurements
43
+
44
+ # @deprecated Use Sentry.get_current_hub instead.
45
+ attr_reader :hub
46
+
47
+ # @deprecated Use Sentry.configuration instead.
48
+ attr_reader :configuration
49
+
50
+ # @deprecated Use Sentry.logger instead.
51
+ attr_reader :logger
52
+
53
+ # The effective sample rate at which this transaction was sampled.
54
+ # @return [Float, nil]
55
+ attr_reader :effective_sample_rate
56
+
57
+ # Additional contexts stored directly on the transaction object.
58
+ # @return [Hash]
59
+ attr_reader :contexts
60
+
61
+ def initialize(
62
+ hub:,
63
+ name: nil,
64
+ source: :custom,
65
+ parent_sampled: nil,
66
+ baggage: nil,
67
+ **options
68
+ )
69
+ super(transaction: self, **options)
70
+
71
+ set_name(name, source: source)
72
+ @parent_sampled = parent_sampled
73
+ @hub = hub
74
+ @baggage = baggage
75
+ @configuration = hub.configuration # to be removed
76
+ @tracing_enabled = hub.configuration.tracing_enabled?
77
+ @traces_sampler = hub.configuration.traces_sampler
78
+ @traces_sample_rate = hub.configuration.traces_sample_rate
79
+ @logger = hub.configuration.logger
80
+ @release = hub.configuration.release
81
+ @environment = hub.configuration.environment
82
+ @dsn = hub.configuration.dsn
83
+ @effective_sample_rate = nil
84
+ @contexts = {}
85
+ @measurements = {}
86
+ init_span_recorder
87
+ end
88
+
89
+ # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
90
+ #
91
+ # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
92
+ #
93
+ # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
94
+ # @param sentry_trace [String] the trace string from the previous transaction.
95
+ # @param baggage [String, nil] the incoming baggage header string.
96
+ # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
97
+ # @param options [Hash] the options you want to use to initialize a Transaction instance.
98
+ # @return [Transaction, nil]
99
+ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
100
+ return unless hub.configuration.tracing_enabled?
101
+ return unless sentry_trace
102
+
103
+ sentry_trace_data = extract_sentry_trace(sentry_trace)
104
+ return unless sentry_trace_data
105
+
106
+ trace_id, parent_span_id, parent_sampled = sentry_trace_data
107
+
108
+ baggage = if baggage && !baggage.empty?
109
+ Baggage.from_incoming_header(baggage)
110
+ else
111
+ # If there's an incoming sentry-trace but no incoming baggage header,
112
+ # for instance in traces coming from older SDKs,
113
+ # baggage will be empty and frozen and won't be populated as head SDK.
114
+ Baggage.new({})
115
+ end
116
+
117
+ baggage.freeze!
118
+
119
+ new(
120
+ trace_id: trace_id,
121
+ parent_span_id: parent_span_id,
122
+ parent_sampled: parent_sampled,
123
+ hub: hub,
124
+ baggage: baggage,
125
+ **options
126
+ )
127
+ end
128
+
129
+ # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
130
+ #
131
+ # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
132
+ # @return [Array, nil]
133
+ def self.extract_sentry_trace(sentry_trace)
134
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
135
+ return nil if match.nil?
136
+
137
+ trace_id, parent_span_id, sampled_flag = match[1..3]
138
+ parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
139
+
140
+ [trace_id, parent_span_id, parent_sampled]
141
+ end
142
+
143
+ # @return [Hash]
144
+ def to_hash
145
+ hash = super
146
+
147
+ hash.merge!(
148
+ name: @name,
149
+ source: @source,
150
+ sampled: @sampled,
151
+ parent_sampled: @parent_sampled
152
+ )
153
+
154
+ hash
155
+ end
156
+
157
+ # @return [Transaction]
158
+ def deep_dup
159
+ copy = super
160
+ copy.init_span_recorder(@span_recorder.max_length)
161
+
162
+ @span_recorder.spans.each do |span|
163
+ # span_recorder's first span is the current span, which should not be added to the copy's spans
164
+ next if span == self
165
+ copy.span_recorder.add(span.dup)
166
+ end
167
+
168
+ copy
169
+ end
170
+
171
+ # Sets a custom measurement on the transaction.
172
+ # @param name [String] name of the measurement
173
+ # @param value [Float] value of the measurement
174
+ # @param unit [String] unit of the measurement
175
+ # @return [void]
176
+ def set_measurement(name, value, unit = "")
177
+ @measurements[name] = { value: value, unit: unit }
178
+ end
179
+
180
+ # Sets initial sampling decision of the transaction.
181
+ # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
182
+ # @return [void]
183
+ def set_initial_sample_decision(sampling_context:)
184
+ unless @tracing_enabled
185
+ @sampled = false
186
+ return
187
+ end
188
+
189
+ unless @sampled.nil?
190
+ @effective_sample_rate = @sampled ? 1.0 : 0.0
191
+ return
192
+ end
193
+
194
+ sample_rate =
195
+ if @traces_sampler.is_a?(Proc)
196
+ @traces_sampler.call(sampling_context)
197
+ elsif !sampling_context[:parent_sampled].nil?
198
+ sampling_context[:parent_sampled]
199
+ else
200
+ @traces_sample_rate
201
+ end
202
+
203
+ transaction_description = generate_transaction_description
204
+
205
+ if [true, false].include?(sample_rate)
206
+ @effective_sample_rate = sample_rate ? 1.0 : 0.0
207
+ elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
208
+ @effective_sample_rate = sample_rate.to_f
209
+ else
210
+ @sampled = false
211
+ log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
212
+ return
213
+ end
214
+
215
+ if sample_rate == 0.0 || sample_rate == false
216
+ @sampled = false
217
+ log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
218
+ return
219
+ end
220
+
221
+ if sample_rate == true
222
+ @sampled = true
223
+ else
224
+ @sampled = Random.rand < sample_rate
225
+ end
226
+
227
+ if @sampled
228
+ log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
229
+ else
230
+ log_debug(
231
+ "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
232
+ )
233
+ end
234
+ end
235
+
236
+ # Finishes the transaction's recording and send it to Sentry.
237
+ # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
238
+ # @return [TransactionEvent]
239
+ def finish(hub: nil, end_timestamp: nil)
240
+ if hub
241
+ log_warn(
242
+ <<~MSG
243
+ Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
244
+ Please use `Hub#start_transaction` with the designated hub.
245
+ MSG
246
+ )
247
+ end
248
+
249
+ hub ||= @hub
250
+
251
+ super(end_timestamp: end_timestamp)
252
+
253
+ if @name.nil?
254
+ @name = UNLABELD_NAME
255
+ end
256
+
257
+ if @sampled
258
+ event = hub.current_client.event_from_transaction(self)
259
+ hub.capture_event(event)
260
+ else
261
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
262
+ end
263
+ end
264
+
265
+ # Get the existing frozen incoming baggage
266
+ # or populate one with sentry- items as the head SDK.
267
+ # @return [Baggage]
268
+ def get_baggage
269
+ populate_head_baggage if @baggage.nil? || @baggage.mutable
270
+ @baggage
271
+ end
272
+
273
+ # Set the transaction name directly.
274
+ # Considered internal api since it bypasses the usual scope logic.
275
+ # @param name [String]
276
+ # @param source [Symbol]
277
+ # @return [void]
278
+ def set_name(name, source: :custom)
279
+ @name = name
280
+ @source = SOURCES.include?(source) ? source.to_sym : :custom
281
+ end
282
+
283
+ # Set contexts directly on the transaction.
284
+ # @param key [String, Symbol]
285
+ # @param value [Object]
286
+ # @return [void]
287
+ def set_context(key, value)
288
+ @contexts[key] = value
289
+ end
290
+
291
+ protected
292
+
293
+ def init_span_recorder(limit = 1000)
294
+ @span_recorder = SpanRecorder.new(limit)
295
+ @span_recorder.add(self)
296
+ end
297
+
298
+ private
299
+
300
+ def generate_transaction_description
301
+ result = op.nil? ? "" : "<#{@op}> "
302
+ result += "transaction"
303
+ result += " <#{@name}>" if @name
304
+ result
305
+ end
306
+
307
+ def populate_head_baggage
308
+ items = {
309
+ "trace_id" => trace_id,
310
+ "sample_rate" => effective_sample_rate&.to_s,
311
+ "environment" => @environment,
312
+ "release" => @release,
313
+ "public_key" => @dsn&.public_key
314
+ }
315
+
316
+ items["transaction"] = name unless source_low_quality?
317
+
318
+ user = @hub.current_scope&.user
319
+ items["user_segment"] = user["segment"] if user && user["segment"]
320
+
321
+ items.compact!
322
+ @baggage = Baggage.new(items, mutable: false)
323
+ end
324
+
325
+ # These are high cardinality and thus bad
326
+ def source_low_quality?
327
+ source == :url
328
+ end
329
+
330
+ class SpanRecorder
331
+ attr_reader :max_length, :spans
332
+
333
+ def initialize(max_length)
334
+ @max_length = max_length
335
+ @spans = []
336
+ end
337
+
338
+ def add(span)
339
+ if @spans.count < @max_length
340
+ @spans << span
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,53 @@
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, nil]
12
+ attr_accessor :dynamic_sampling_context
13
+
14
+ # @return [Hash]
15
+ attr_accessor :measurements
16
+
17
+ # @return [Float, nil]
18
+ attr_reader :start_timestamp
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
+ end
36
+
37
+ # Sets the event's start_timestamp.
38
+ # @param time [Time, Float]
39
+ # @return [void]
40
+ def start_timestamp=(time)
41
+ @start_timestamp = time.is_a?(Time) ? time.to_f : time
42
+ end
43
+
44
+ # @return [Hash]
45
+ def to_hash
46
+ data = super
47
+ data[:spans] = @spans.map(&:to_hash) if @spans
48
+ data[:start_timestamp] = @start_timestamp
49
+ data[:measurements] = @measurements
50
+ data
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Transport
5
+ class Configuration
6
+ attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
7
+ attr_reader :transport_class
8
+
9
+ def initialize
10
+ @ssl_verification = true
11
+ @open_timeout = 1
12
+ @timeout = 2
13
+ @encoding = HTTPTransport::GZIP_ENCODING
14
+ end
15
+
16
+ def transport_class=(klass)
17
+ unless klass.is_a?(Class)
18
+ raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}")
19
+ end
20
+
21
+ @transport_class = klass
22
+ end
23
+ end
24
+ end
25
+ 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
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
5
+
6
+ module Sentry
7
+ class HTTPTransport < Transport
8
+ GZIP_ENCODING = "gzip"
9
+ GZIP_THRESHOLD = 1024 * 30
10
+ CONTENT_TYPE = 'application/x-sentry-envelope'
11
+
12
+ DEFAULT_DELAY = 60
13
+ RETRY_AFTER_HEADER = "retry-after"
14
+ RATE_LIMIT_HEADER = "x-sentry-rate-limits"
15
+ USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
16
+
17
+ def initialize(*args)
18
+ super
19
+ @endpoint = @dsn.envelope_endpoint
20
+
21
+ log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
22
+ end
23
+
24
+ def send_data(data)
25
+ encoding = ""
26
+
27
+ if should_compress?(data)
28
+ data = Zlib.gzip(data)
29
+ encoding = GZIP_ENCODING
30
+ end
31
+
32
+ headers = {
33
+ 'Content-Type' => CONTENT_TYPE,
34
+ 'Content-Encoding' => encoding,
35
+ 'X-Sentry-Auth' => generate_auth_header,
36
+ 'User-Agent' => USER_AGENT
37
+ }
38
+
39
+ response = conn.start do |http|
40
+ request = ::Net::HTTP::Post.new(@endpoint, headers)
41
+ request.body = data
42
+ http.request(request)
43
+ end
44
+
45
+ if response.code.match?(/\A2\d{2}/)
46
+ if has_rate_limited_header?(response)
47
+ handle_rate_limited_response(response)
48
+ end
49
+ else
50
+ error_info = "the server responded with status #{response.code}"
51
+
52
+ if response.code == "429"
53
+ handle_rate_limited_response(response)
54
+ else
55
+ error_info += "\nbody: #{response.body}"
56
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
57
+ end
58
+
59
+ raise Sentry::ExternalError, error_info
60
+ end
61
+ rescue SocketError => e
62
+ raise Sentry::ExternalError.new(e.message)
63
+ end
64
+
65
+ private
66
+
67
+ def has_rate_limited_header?(headers)
68
+ headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
69
+ end
70
+
71
+ def handle_rate_limited_response(headers)
72
+ rate_limits =
73
+ if rate_limits = headers[RATE_LIMIT_HEADER]
74
+ parse_rate_limit_header(rate_limits)
75
+ elsif retry_after = headers[RETRY_AFTER_HEADER]
76
+ # although Sentry doesn't send a date string back
77
+ # based on HTTP specification, this could be a date string (instead of an integer)
78
+ retry_after = retry_after.to_i
79
+ retry_after = DEFAULT_DELAY if retry_after == 0
80
+
81
+ { nil => Time.now + retry_after }
82
+ else
83
+ { nil => Time.now + DEFAULT_DELAY }
84
+ end
85
+
86
+ rate_limits.each do |category, limit|
87
+ if current_limit = @rate_limits[category]
88
+ if current_limit < limit
89
+ @rate_limits[category] = limit
90
+ end
91
+ else
92
+ @rate_limits[category] = limit
93
+ end
94
+ end
95
+ end
96
+
97
+ def parse_rate_limit_header(rate_limit_header)
98
+ time = Time.now
99
+
100
+ result = {}
101
+
102
+ limits = rate_limit_header.split(",")
103
+ limits.each do |limit|
104
+ next if limit.nil? || limit.empty?
105
+
106
+ begin
107
+ retry_after, categories = limit.strip.split(":").first(2)
108
+ retry_after = time + retry_after.to_i
109
+ categories = categories.split(";")
110
+
111
+ if categories.empty?
112
+ result[nil] = retry_after
113
+ else
114
+ categories.each do |category|
115
+ result[category] = retry_after
116
+ end
117
+ end
118
+ rescue StandardError
119
+ end
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ def should_compress?(data)
126
+ @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
127
+ end
128
+
129
+ def conn
130
+ server = URI(@dsn.server)
131
+
132
+ connection =
133
+ if proxy = normalize_proxy(@transport_configuration.proxy)
134
+ ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
135
+ else
136
+ ::Net::HTTP.new(server.hostname, server.port, nil)
137
+ end
138
+
139
+ connection.use_ssl = server.scheme == "https"
140
+ connection.read_timeout = @transport_configuration.timeout
141
+ connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
142
+ connection.open_timeout = @transport_configuration.open_timeout
143
+
144
+ ssl_configuration.each do |key, value|
145
+ connection.send("#{key}=", value)
146
+ end
147
+
148
+ connection
149
+ end
150
+
151
+ def normalize_proxy(proxy)
152
+ return proxy unless proxy
153
+
154
+ case proxy
155
+ when String
156
+ uri = URI(proxy)
157
+ { uri: uri, user: uri.user, password: uri.password }
158
+ when URI
159
+ { uri: proxy, user: proxy.user, password: proxy.password }
160
+ when Hash
161
+ proxy
162
+ end
163
+ end
164
+
165
+ def ssl_configuration
166
+ configuration = {
167
+ verify: @transport_configuration.ssl_verification,
168
+ ca_file: @transport_configuration.ssl_ca_file
169
+ }.merge(@transport_configuration.ssl || {})
170
+
171
+ configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
172
+ configuration
173
+ end
174
+ end
175
+ end