sentry-ruby 5.10.0 → 5.17.3

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -13
  3. data/README.md +10 -10
  4. data/Rakefile +1 -1
  5. data/lib/sentry/background_worker.rb +9 -2
  6. data/lib/sentry/backpressure_monitor.rb +75 -0
  7. data/lib/sentry/backtrace.rb +7 -3
  8. data/lib/sentry/breadcrumb.rb +8 -2
  9. data/lib/sentry/check_in_event.rb +60 -0
  10. data/lib/sentry/client.rb +88 -17
  11. data/lib/sentry/configuration.rb +66 -12
  12. data/lib/sentry/cron/configuration.rb +23 -0
  13. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  14. data/lib/sentry/cron/monitor_config.rb +53 -0
  15. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  16. data/lib/sentry/dsn.rb +1 -1
  17. data/lib/sentry/envelope.rb +19 -2
  18. data/lib/sentry/error_event.rb +2 -2
  19. data/lib/sentry/event.rb +14 -36
  20. data/lib/sentry/hub.rb +70 -2
  21. data/lib/sentry/integrable.rb +10 -0
  22. data/lib/sentry/interface.rb +1 -0
  23. data/lib/sentry/interfaces/exception.rb +5 -3
  24. data/lib/sentry/interfaces/mechanism.rb +20 -0
  25. data/lib/sentry/interfaces/request.rb +2 -2
  26. data/lib/sentry/interfaces/single_exception.rb +10 -6
  27. data/lib/sentry/interfaces/stacktrace_builder.rb +8 -0
  28. data/lib/sentry/metrics/aggregator.rb +276 -0
  29. data/lib/sentry/metrics/configuration.rb +47 -0
  30. data/lib/sentry/metrics/counter_metric.rb +25 -0
  31. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  32. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  33. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  34. data/lib/sentry/metrics/metric.rb +19 -0
  35. data/lib/sentry/metrics/set_metric.rb +28 -0
  36. data/lib/sentry/metrics/timing.rb +43 -0
  37. data/lib/sentry/metrics.rb +55 -0
  38. data/lib/sentry/net/http.rb +25 -22
  39. data/lib/sentry/profiler.rb +18 -7
  40. data/lib/sentry/propagation_context.rb +135 -0
  41. data/lib/sentry/puma.rb +12 -5
  42. data/lib/sentry/rack/capture_exceptions.rb +7 -5
  43. data/lib/sentry/rake.rb +3 -14
  44. data/lib/sentry/redis.rb +8 -3
  45. data/lib/sentry/release_detector.rb +1 -1
  46. data/lib/sentry/scope.rb +36 -15
  47. data/lib/sentry/session.rb +2 -2
  48. data/lib/sentry/session_flusher.rb +1 -6
  49. data/lib/sentry/span.rb +54 -3
  50. data/lib/sentry/test_helper.rb +18 -12
  51. data/lib/sentry/transaction.rb +33 -33
  52. data/lib/sentry/transaction_event.rb +5 -3
  53. data/lib/sentry/transport/configuration.rb +73 -1
  54. data/lib/sentry/transport/http_transport.rb +68 -37
  55. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  56. data/lib/sentry/transport.rb +27 -37
  57. data/lib/sentry/utils/argument_checking_helper.rb +12 -0
  58. data/lib/sentry/utils/real_ip.rb +1 -1
  59. data/lib/sentry/utils/request_id.rb +1 -1
  60. data/lib/sentry/version.rb +1 -1
  61. data/lib/sentry-ruby.rb +96 -26
  62. data/sentry-ruby.gemspec +1 -0
  63. metadata +35 -2
data/lib/sentry/span.rb CHANGED
@@ -1,9 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
+ require "sentry/metrics/local_aggregator"
4
5
 
5
6
  module Sentry
6
7
  class Span
8
+ # We will try to be consistent with OpenTelemetry on this front going forward.
9
+ # https://develop.sentry.dev/sdk/performance/span-data-conventions/
10
+ module DataConventions
11
+ URL = "url"
12
+ HTTP_STATUS_CODE = "http.response.status_code"
13
+ HTTP_QUERY = "http.query"
14
+ HTTP_METHOD = "http.request.method"
15
+
16
+ # An identifier for the database management system (DBMS) product being used.
17
+ # Example: postgresql
18
+ DB_SYSTEM = "db.system"
19
+
20
+ # The name of the database being accessed.
21
+ # For commands that switch the database, this should be set to the target database
22
+ # (even if the command fails).
23
+ # Example: myDatabase
24
+ DB_NAME = "db.name"
25
+
26
+ # Name of the database host.
27
+ # Example: example.com
28
+ SERVER_ADDRESS = "server.address"
29
+
30
+ # Logical server port number
31
+ # Example: 80; 8080; 443
32
+ SERVER_PORT = "server.port"
33
+
34
+ # Physical server IP address or Unix socket address.
35
+ # Example: 10.5.3.2
36
+ SERVER_SOCKET_ADDRESS = "server.socket.address"
37
+
38
+ # Physical server port.
39
+ # Recommended: If different than server.port.
40
+ # Example: 16456
41
+ SERVER_SOCKET_PORT = "server.socket.port"
42
+ end
43
+
7
44
  STATUS_MAP = {
8
45
  400 => "invalid_argument",
9
46
  401 => "unauthenticated",
@@ -75,7 +112,7 @@ module Sentry
75
112
  timestamp: nil
76
113
  )
77
114
  @trace_id = trace_id || SecureRandom.uuid.delete("-")
78
- @span_id = span_id || SecureRandom.hex(8)
115
+ @span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16)
79
116
  @parent_span_id = parent_span_id
80
117
  @sampled = sampled
81
118
  @start_timestamp = start_timestamp || Sentry.utc_now.to_f
@@ -113,7 +150,7 @@ module Sentry
113
150
 
114
151
  # @return [Hash]
115
152
  def to_hash
116
- {
153
+ hash = {
117
154
  trace_id: @trace_id,
118
155
  span_id: @span_id,
119
156
  parent_span_id: @parent_span_id,
@@ -125,6 +162,11 @@ module Sentry
125
162
  tags: @tags,
126
163
  data: @data
127
164
  }
165
+
166
+ summary = metrics_summary
167
+ hash[:_metrics_summary] = summary if summary
168
+
169
+ hash
128
170
  end
129
171
 
130
172
  # Returns the span's context that can be used to embed in an Event.
@@ -208,7 +250,7 @@ module Sentry
208
250
  # @param status_code [String] example: "500".
209
251
  def set_http_status(status_code)
210
252
  status_code = status_code.to_i
211
- set_data("status_code", status_code)
253
+ set_data(DataConventions::HTTP_STATUS_CODE, status_code)
212
254
 
213
255
  status =
214
256
  if status_code >= 200 && status_code < 299
@@ -232,5 +274,14 @@ module Sentry
232
274
  def set_tag(key, value)
233
275
  @tags[key] = value
234
276
  end
277
+
278
+ # Collects gauge metrics on the span for metric summaries.
279
+ def metrics_local_aggregator
280
+ @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new
281
+ end
282
+
283
+ def metrics_summary
284
+ @metrics_local_aggregator&.to_hash
285
+ end
235
286
  end
236
287
  end
@@ -14,24 +14,28 @@ module Sentry
14
14
  # @return [void]
15
15
  def setup_sentry_test(&block)
16
16
  raise "please make sure the SDK is initialized for testing" unless Sentry.initialized?
17
- copied_config = Sentry.configuration.dup
17
+ dummy_config = Sentry.configuration.dup
18
18
  # configure dummy DSN, so the events will not be sent to the actual service
19
- copied_config.dsn = DUMMY_DSN
19
+ dummy_config.dsn = DUMMY_DSN
20
20
  # set transport to DummyTransport, so we can easily intercept the captured events
21
- copied_config.transport.transport_class = Sentry::DummyTransport
21
+ dummy_config.transport.transport_class = Sentry::DummyTransport
22
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)
23
+ dummy_config.enabled_environments << dummy_config.environment unless dummy_config.enabled_environments.include?(dummy_config.environment)
24
24
  # disble async event sending
25
- copied_config.background_worker_threads = 0
25
+ dummy_config.background_worker_threads = 0
26
26
 
27
27
  # user can overwrite some of the configs, with a few exceptions like:
28
28
  # - include_local_variables
29
29
  # - auto_session_tracking
30
- block&.call(copied_config)
30
+ block&.call(dummy_config)
31
31
 
32
- test_client = Sentry::Client.new(copied_config)
32
+ # the base layer's client should already use the dummy config so nothing will be sent by accident
33
+ base_client = Sentry::Client.new(dummy_config)
34
+ Sentry.get_current_hub.bind_client(base_client)
35
+ # create a new layer so mutations made to the testing scope or configuration could be simply popped later
36
+ Sentry.get_current_hub.push_scope
37
+ test_client = Sentry::Client.new(dummy_config.dup)
33
38
  Sentry.get_current_hub.bind_client(test_client)
34
- Sentry.get_current_scope.clear
35
39
  end
36
40
 
37
41
  # Clears all stored events and envelopes.
@@ -40,9 +44,12 @@ module Sentry
40
44
  def teardown_sentry_test
41
45
  return unless Sentry.initialized?
42
46
 
43
- sentry_transport.events = []
44
- sentry_transport.envelopes = []
45
- Sentry.get_current_scope.clear
47
+ # pop testing layer created by `setup_sentry_test`
48
+ # but keep the base layer to avoid nil-pointer errors
49
+ # TODO: find a way to notify users if they somehow popped the test layer before calling this method
50
+ if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1
51
+ Sentry.get_current_hub.pop_scope
52
+ end
46
53
  end
47
54
 
48
55
  # @return [Transport]
@@ -75,4 +82,3 @@ module Sentry
75
82
  end
76
83
  end
77
84
  end
78
-
@@ -2,21 +2,18 @@
2
2
 
3
3
  require "sentry/baggage"
4
4
  require "sentry/profiler"
5
+ require "sentry/propagation_context"
5
6
 
6
7
  module Sentry
7
8
  class Transaction < Span
8
- SENTRY_TRACE_REGEXP = Regexp.new(
9
- "^[ \t]*" + # whitespace
10
- "([0-9a-f]{32})?" + # trace_id
11
- "-?([0-9a-f]{16})?" + # span_id
12
- "-?([01])?" + # sampled
13
- "[ \t]*$" # whitespace
14
- )
9
+ # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
10
+ SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
11
+
15
12
  UNLABELD_NAME = "<unlabeled transaction>".freeze
16
13
  MESSAGE_PREFIX = "[Tracing]"
17
14
 
18
15
  # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
19
- SOURCES = %i(custom url route view component task)
16
+ SOURCES = %i[custom url route view component task]
20
17
 
21
18
  include LoggingHelper
22
19
 
@@ -92,6 +89,8 @@ module Sentry
92
89
  init_span_recorder
93
90
  end
94
91
 
92
+ # @deprecated use Sentry.continue_trace instead.
93
+ #
95
94
  # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
96
95
  #
97
96
  # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
@@ -111,14 +110,15 @@ module Sentry
111
110
 
112
111
  trace_id, parent_span_id, parent_sampled = sentry_trace_data
113
112
 
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
113
+ baggage =
114
+ 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
122
 
123
123
  baggage.freeze!
124
124
 
@@ -132,18 +132,10 @@ module Sentry
132
132
  )
133
133
  end
134
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.
135
+ # @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead.
138
136
  # @return [Array, nil]
139
137
  def self.extract_sentry_trace(sentry_trace)
140
- match = SENTRY_TRACE_REGEXP.match(sentry_trace)
141
- return nil if match.nil?
142
-
143
- trace_id, parent_span_id, sampled_flag = match[1..3]
144
- parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
145
-
146
- [trace_id, parent_span_id, parent_sampled]
138
+ PropagationContext.extract_sentry_trace(sentry_trace)
147
139
  end
148
140
 
149
141
  # @return [Hash]
@@ -227,7 +219,12 @@ module Sentry
227
219
  if sample_rate == true
228
220
  @sampled = true
229
221
  else
230
- @sampled = Random.rand < sample_rate
222
+ if Sentry.backpressure_monitor
223
+ factor = Sentry.backpressure_monitor.downsample_factor
224
+ @effective_sample_rate /= 2**factor
225
+ end
226
+
227
+ @sampled = Random.rand < @effective_sample_rate
231
228
  end
232
229
 
233
230
  if @sampled
@@ -266,7 +263,9 @@ module Sentry
266
263
  event = hub.current_client.event_from_transaction(self)
267
264
  hub.capture_event(event)
268
265
  else
269
- hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
266
+ is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive?
267
+ reason = is_backpressure ? :backpressure : :sample_rate
268
+ hub.current_client.transport.record_lost_event(reason, 'transaction')
270
269
  end
271
270
  end
272
271
 
@@ -303,6 +302,11 @@ module Sentry
303
302
  profiler.start
304
303
  end
305
304
 
305
+ # These are high cardinality and thus bad
306
+ def source_low_quality?
307
+ source == :url
308
+ end
309
+
306
310
  protected
307
311
 
308
312
  def init_span_recorder(limit = 1000)
@@ -323,6 +327,7 @@ module Sentry
323
327
  items = {
324
328
  "trace_id" => trace_id,
325
329
  "sample_rate" => effective_sample_rate&.to_s,
330
+ "sampled" => sampled&.to_s,
326
331
  "environment" => @environment,
327
332
  "release" => @release,
328
333
  "public_key" => @dsn&.public_key
@@ -337,11 +342,6 @@ module Sentry
337
342
  @baggage = Baggage.new(items, mutable: false)
338
343
  end
339
344
 
340
- # These are high cardinality and thus bad
341
- def source_low_quality?
342
- source == :url
343
- end
344
-
345
345
  class SpanRecorder
346
346
  attr_reader :max_length, :spans
347
347
 
@@ -8,9 +8,6 @@ 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
11
  # @return [Hash]
15
12
  attr_accessor :measurements
16
13
 
@@ -20,6 +17,9 @@ module Sentry
20
17
  # @return [Hash, nil]
21
18
  attr_accessor :profile
22
19
 
20
+ # @return [Hash, nil]
21
+ attr_accessor :metrics_summary
22
+
23
23
  def initialize(transaction:, **options)
24
24
  super(**options)
25
25
 
@@ -32,6 +32,7 @@ module Sentry
32
32
  self.tags = transaction.tags
33
33
  self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
34
34
  self.measurements = transaction.measurements
35
+ self.metrics_summary = transaction.metrics_summary
35
36
 
36
37
  finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
37
38
  self.spans = finished_spans.map(&:to_hash)
@@ -52,6 +53,7 @@ module Sentry
52
53
  data[:spans] = @spans.map(&:to_hash) if @spans
53
54
  data[:start_timestamp] = @start_timestamp
54
55
  data[:measurements] = @measurements
56
+ data[:_metrics_summary] = @metrics_summary if @metrics_summary
55
57
  data
56
58
  end
57
59
 
@@ -3,7 +3,79 @@
3
3
  module Sentry
4
4
  class Transport
5
5
  class Configuration
6
- attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
6
+ # The timeout in seconds to open a connection to Sentry, in seconds.
7
+ # Default value is 2.
8
+ #
9
+ # @return [Integer]
10
+ attr_accessor :timeout
11
+
12
+ # The timeout in seconds to read data from Sentry, in seconds.
13
+ # Default value is 1.
14
+ #
15
+ # @return [Integer]
16
+ attr_accessor :open_timeout
17
+
18
+ # The proxy configuration to use to connect to Sentry.
19
+ # Accepts either a URI formatted string, URI, or a hash with the `uri`,
20
+ # `user`, and `password` keys.
21
+ #
22
+ # @example
23
+ # # setup proxy using a string:
24
+ # config.transport.proxy = "https://user:password@proxyhost:8080"
25
+ #
26
+ # # setup proxy using a URI:
27
+ # config.transport.proxy = URI("https://user:password@proxyhost:8080")
28
+ #
29
+ # # setup proxy using a hash:
30
+ # config.transport.proxy = {
31
+ # uri: URI("https://proxyhost:8080"),
32
+ # user: "user",
33
+ # password: "password"
34
+ # }
35
+ #
36
+ # If you're using the default transport (`Sentry::HTTPTransport`),
37
+ # proxy settings will also automatically be read from tne environment
38
+ # variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`).
39
+ #
40
+ # @return [String, URI, Hash, nil]
41
+ attr_accessor :proxy
42
+
43
+ # The SSL configuration to use to connect to Sentry.
44
+ # You can either pass a `Hash` containing `ca_file` and `verification` keys,
45
+ # or you can set those options directly on the `Sentry::HTTPTransport::Configuration` object:
46
+ #
47
+ # @example
48
+ # config.transport.ssl = {
49
+ # ca_file: "/path/to/ca_file",
50
+ # verification: true
51
+ # end
52
+ #
53
+ # @return [Hash, nil]
54
+ attr_accessor :ssl
55
+
56
+ # The path to the CA file to use to verify the SSL connection.
57
+ # Default value is `nil`.
58
+ #
59
+ # @return [String, nil]
60
+ attr_accessor :ssl_ca_file
61
+
62
+ # Whether to verify that the peer certificate is valid in SSL connections.
63
+ # Default value is `true`.
64
+ #
65
+ # @return [Boolean]
66
+ attr_accessor :ssl_verification
67
+
68
+ # The encoding to use to compress the request body.
69
+ # Default value is `Sentry::HTTPTransport::GZIP_ENCODING`.
70
+ #
71
+ # @return [String]
72
+ attr_accessor :encoding
73
+
74
+ # The class to use as a transport to connect to Sentry.
75
+ # If this option not set, it will return `nil`, and Sentry will use
76
+ # `Sentry::HTTPTransport` by default.
77
+ #
78
+ # @return [Class, nil]
7
79
  attr_reader :transport_class
8
80
 
9
81
  def initialize
@@ -14,11 +14,19 @@ module Sentry
14
14
  RATE_LIMIT_HEADER = "x-sentry-rate-limits"
15
15
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
16
16
 
17
+ # The list of errors ::Net::HTTP is known to raise
18
+ # See https://github.com/ruby/ruby/blob/b0c639f249165d759596f9579fa985cb30533de6/lib/bundler/fetcher.rb#L281-L286
19
+ HTTP_ERRORS = [
20
+ Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH,
21
+ Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
22
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
23
+ Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
24
+ ].freeze
25
+
26
+
17
27
  def initialize(*args)
18
28
  super
19
- @endpoint = @dsn.envelope_endpoint
20
-
21
- log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
29
+ log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
22
30
  end
23
31
 
24
32
  def send_data(data)
@@ -32,34 +40,76 @@ module Sentry
32
40
  headers = {
33
41
  'Content-Type' => CONTENT_TYPE,
34
42
  'Content-Encoding' => encoding,
35
- 'X-Sentry-Auth' => generate_auth_header,
36
43
  'User-Agent' => USER_AGENT
37
44
  }
38
45
 
46
+ auth_header = generate_auth_header
47
+ headers['X-Sentry-Auth'] = auth_header if auth_header
48
+
39
49
  response = conn.start do |http|
40
- request = ::Net::HTTP::Post.new(@endpoint, headers)
50
+ request = ::Net::HTTP::Post.new(endpoint, headers)
41
51
  request.body = data
42
52
  http.request(request)
43
53
  end
44
54
 
45
55
  if response.code.match?(/\A2\d{2}/)
46
- if has_rate_limited_header?(response)
47
- handle_rate_limited_response(response)
48
- end
56
+ handle_rate_limited_response(response) if has_rate_limited_header?(response)
57
+ elsif response.code == "429"
58
+ log_debug("the server responded with status 429")
59
+ handle_rate_limited_response(response)
49
60
  else
50
61
  error_info = "the server responded with status #{response.code}"
62
+ error_info += "\nbody: #{response.body}"
63
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
64
+
65
+ raise Sentry::ExternalError, error_info
66
+ end
67
+ rescue SocketError, *HTTP_ERRORS => e
68
+ on_error if respond_to?(:on_error)
69
+ raise Sentry::ExternalError.new(e&.message)
70
+ end
71
+
72
+ def endpoint
73
+ @dsn.envelope_endpoint
74
+ end
75
+
76
+ def generate_auth_header
77
+ return nil unless @dsn
78
+
79
+ now = Sentry.utc_now.to_i
80
+ fields = {
81
+ 'sentry_version' => PROTOCOL_VERSION,
82
+ 'sentry_client' => USER_AGENT,
83
+ 'sentry_timestamp' => now,
84
+ 'sentry_key' => @dsn.public_key
85
+ }
86
+ fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
87
+ 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
88
+ end
51
89
 
52
- if response.code == "429"
53
- handle_rate_limited_response(response)
90
+ def conn
91
+ server = URI(@dsn.server)
92
+
93
+ # connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
94
+ # Net::HTTP will automatically read the env vars.
95
+ # See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
96
+ connection =
97
+ if proxy = normalize_proxy(@transport_configuration.proxy)
98
+ ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
54
99
  else
55
- error_info += "\nbody: #{response.body}"
56
- error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
100
+ ::Net::HTTP.new(server.hostname, server.port)
57
101
  end
58
102
 
59
- raise Sentry::ExternalError, error_info
103
+ connection.use_ssl = server.scheme == "https"
104
+ connection.read_timeout = @transport_configuration.timeout
105
+ connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
106
+ connection.open_timeout = @transport_configuration.open_timeout
107
+
108
+ ssl_configuration.each do |key, value|
109
+ connection.send("#{key}=", value)
60
110
  end
61
- rescue SocketError => e
62
- raise Sentry::ExternalError.new(e.message)
111
+
112
+ connection
63
113
  end
64
114
 
65
115
  private
@@ -126,28 +176,9 @@ module Sentry
126
176
  @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
127
177
  end
128
178
 
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
-
179
+ # @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`.
180
+ # Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys.
181
+ # @return [Hash] Normalized proxy config that will be passed into `Net::HTTP`
151
182
  def normalize_proxy(proxy)
152
183
  return proxy unless proxy
153
184
 
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "zlib"
5
+
6
+ module Sentry
7
+ # Designed to just report events to Spotlight in development.
8
+ class SpotlightTransport < HTTPTransport
9
+ DEFAULT_SIDECAR_URL = "http://localhost:8969/stream"
10
+ MAX_FAILED_REQUESTS = 3
11
+
12
+ def initialize(configuration)
13
+ super
14
+ @sidecar_url = configuration.spotlight.is_a?(String) ? configuration.spotlight : DEFAULT_SIDECAR_URL
15
+ @failed = 0
16
+ @logged = false
17
+
18
+ log_debug("[Spotlight] initialized for url #{@sidecar_url}")
19
+ end
20
+
21
+ def endpoint
22
+ "/stream"
23
+ end
24
+
25
+ def send_data(data)
26
+ if @failed >= MAX_FAILED_REQUESTS
27
+ unless @logged
28
+ log_debug("[Spotlight] disabling because of too many request failures")
29
+ @logged = true
30
+ end
31
+
32
+ return
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def on_error
39
+ @failed += 1
40
+ end
41
+
42
+ # Similar to HTTPTransport connection, but does not support Proxy and SSL
43
+ def conn
44
+ sidecar = URI(@sidecar_url)
45
+ connection = ::Net::HTTP.new(sidecar.hostname, sidecar.port, nil)
46
+ connection.use_ssl = false
47
+ connection
48
+ end
49
+ end
50
+ end