sentry-ruby 5.3.0 → 5.8.0

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