sentry-ruby 5.13.0 → 5.21.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -18
  3. data/README.md +20 -10
  4. data/Rakefile +3 -1
  5. data/bin/console +2 -0
  6. data/lib/sentry/attachment.rb +40 -0
  7. data/lib/sentry/background_worker.rb +9 -2
  8. data/lib/sentry/backpressure_monitor.rb +45 -0
  9. data/lib/sentry/backtrace.rb +10 -8
  10. data/lib/sentry/baggage.rb +7 -7
  11. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  12. data/lib/sentry/check_in_event.rb +5 -5
  13. data/lib/sentry/client.rb +71 -18
  14. data/lib/sentry/configuration.rb +108 -32
  15. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  16. data/lib/sentry/cron/configuration.rb +23 -0
  17. data/lib/sentry/cron/monitor_check_ins.rb +42 -26
  18. data/lib/sentry/cron/monitor_config.rb +1 -1
  19. data/lib/sentry/cron/monitor_schedule.rb +1 -1
  20. data/lib/sentry/dsn.rb +4 -4
  21. data/lib/sentry/envelope/item.rb +88 -0
  22. data/lib/sentry/envelope.rb +2 -68
  23. data/lib/sentry/error_event.rb +2 -2
  24. data/lib/sentry/event.rb +20 -46
  25. data/lib/sentry/faraday.rb +77 -0
  26. data/lib/sentry/graphql.rb +9 -0
  27. data/lib/sentry/hub.rb +25 -5
  28. data/lib/sentry/integrable.rb +4 -0
  29. data/lib/sentry/interface.rb +1 -0
  30. data/lib/sentry/interfaces/exception.rb +5 -3
  31. data/lib/sentry/interfaces/mechanism.rb +20 -0
  32. data/lib/sentry/interfaces/request.rb +7 -7
  33. data/lib/sentry/interfaces/single_exception.rb +10 -7
  34. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  35. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  36. data/lib/sentry/logger.rb +1 -1
  37. data/lib/sentry/metrics/aggregator.rb +248 -0
  38. data/lib/sentry/metrics/configuration.rb +47 -0
  39. data/lib/sentry/metrics/counter_metric.rb +25 -0
  40. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  41. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  42. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  43. data/lib/sentry/metrics/metric.rb +19 -0
  44. data/lib/sentry/metrics/set_metric.rb +28 -0
  45. data/lib/sentry/metrics/timing.rb +43 -0
  46. data/lib/sentry/metrics.rb +56 -0
  47. data/lib/sentry/net/http.rb +22 -39
  48. data/lib/sentry/profiler/helpers.rb +46 -0
  49. data/lib/sentry/profiler.rb +25 -56
  50. data/lib/sentry/propagation_context.rb +10 -9
  51. data/lib/sentry/puma.rb +1 -1
  52. data/lib/sentry/rack/capture_exceptions.rb +16 -4
  53. data/lib/sentry/rack.rb +2 -2
  54. data/lib/sentry/rake.rb +4 -15
  55. data/lib/sentry/redis.rb +2 -1
  56. data/lib/sentry/release_detector.rb +5 -5
  57. data/lib/sentry/scope.rb +48 -37
  58. data/lib/sentry/session.rb +2 -2
  59. data/lib/sentry/session_flusher.rb +7 -39
  60. data/lib/sentry/span.rb +46 -5
  61. data/lib/sentry/test_helper.rb +5 -2
  62. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  63. data/lib/sentry/transaction.rb +27 -18
  64. data/lib/sentry/transaction_event.rb +6 -2
  65. data/lib/sentry/transport/configuration.rb +73 -1
  66. data/lib/sentry/transport/http_transport.rb +72 -41
  67. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  68. data/lib/sentry/transport.rb +36 -41
  69. data/lib/sentry/utils/argument_checking_helper.rb +6 -0
  70. data/lib/sentry/utils/env_helper.rb +21 -0
  71. data/lib/sentry/utils/http_tracing.rb +41 -0
  72. data/lib/sentry/utils/logging_helper.rb +0 -4
  73. data/lib/sentry/utils/real_ip.rb +2 -2
  74. data/lib/sentry/utils/request_id.rb +1 -1
  75. data/lib/sentry/vernier/output.rb +89 -0
  76. data/lib/sentry/vernier/profiler.rb +125 -0
  77. data/lib/sentry/version.rb +1 -1
  78. data/lib/sentry-ruby.rb +61 -27
  79. data/sentry-ruby-core.gemspec +3 -1
  80. data/sentry-ruby.gemspec +15 -6
  81. metadata +47 -7
@@ -1,58 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- class SessionFlusher
5
- include LoggingHelper
6
-
4
+ class SessionFlusher < ThreadedPeriodicWorker
7
5
  FLUSH_INTERVAL = 60
8
6
 
9
7
  def initialize(configuration, client)
10
- @thread = nil
11
- @exited = false
8
+ super(configuration.logger, FLUSH_INTERVAL)
12
9
  @client = client
13
10
  @pending_aggregates = {}
14
11
  @release = configuration.release
15
12
  @environment = configuration.environment
16
- @logger = configuration.logger
17
13
 
18
14
  log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release
19
15
  end
20
16
 
21
17
  def flush
22
18
  return if @pending_aggregates.empty?
23
- envelope = pending_envelope
24
-
25
- Sentry.background_worker.perform do
26
- @client.transport.send_envelope(envelope)
27
- end
28
19
 
20
+ @client.capture_envelope(pending_envelope)
29
21
  @pending_aggregates = {}
30
22
  end
31
23
 
24
+ alias_method :run, :flush
25
+
32
26
  def add_session(session)
33
- return if @exited
34
27
  return unless @release
35
28
 
36
- begin
37
- ensure_thread
38
- rescue ThreadError
39
- log_debug("Session flusher thread creation failed")
40
- @exited = true
41
- return
42
- end
29
+ return unless ensure_thread
43
30
 
44
31
  return unless Session::AGGREGATE_STATUSES.include?(session.status)
45
32
  @pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key)
46
33
  @pending_aggregates[session.aggregation_key][session.status] += 1
47
34
  end
48
35
 
49
- def kill
50
- log_debug("Killing session flusher")
51
-
52
- @exited = true
53
- @thread&.kill
54
- end
55
-
56
36
  private
57
37
 
58
38
  def init_aggregates(aggregation_key)
@@ -64,7 +44,7 @@ module Sentry
64
44
  def pending_envelope
65
45
  envelope = Envelope.new
66
46
 
67
- header = { type: 'sessions' }
47
+ header = { type: "sessions" }
68
48
  payload = { attrs: attrs, aggregates: @pending_aggregates.values }
69
49
 
70
50
  envelope.add_item(header, payload)
@@ -74,17 +54,5 @@ module Sentry
74
54
  def attrs
75
55
  { release: @release, environment: @environment }
76
56
  end
77
-
78
- def ensure_thread
79
- return if @thread&.alive?
80
-
81
- @thread = Thread.new do
82
- loop do
83
- sleep(FLUSH_INTERVAL)
84
- flush
85
- end
86
- end
87
- end
88
-
89
57
  end
90
58
  end
data/lib/sentry/span.rb CHANGED
@@ -1,10 +1,10 @@
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
7
-
8
8
  # We will try to be consistent with OpenTelemetry on this front going forward.
9
9
  # https://develop.sentry.dev/sdk/performance/span-data-conventions/
10
10
  module DataConventions
@@ -39,6 +39,11 @@ module Sentry
39
39
  # Recommended: If different than server.port.
40
40
  # Example: 16456
41
41
  SERVER_SOCKET_PORT = "server.socket.port"
42
+
43
+ FILEPATH = "code.filepath"
44
+ LINENO = "code.lineno"
45
+ FUNCTION = "code.function"
46
+ NAMESPACE = "code.namespace"
42
47
  end
43
48
 
44
49
  STATUS_MAP = {
@@ -55,6 +60,8 @@ module Sentry
55
60
  504 => "deadline_exceeded"
56
61
  }
57
62
 
63
+ DEFAULT_SPAN_ORIGIN = "manual"
64
+
58
65
  # An uuid that can be used to identify a trace.
59
66
  # @return [String]
60
67
  attr_reader :trace_id
@@ -88,6 +95,9 @@ module Sentry
88
95
  # Span data
89
96
  # @return [Hash]
90
97
  attr_reader :data
98
+ # Span origin that tracks what kind of instrumentation created a span
99
+ # @return [String]
100
+ attr_reader :origin
91
101
 
92
102
  # The SpanRecorder the current span belongs to.
93
103
  # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself).
@@ -109,7 +119,8 @@ module Sentry
109
119
  parent_span_id: nil,
110
120
  sampled: nil,
111
121
  start_timestamp: nil,
112
- timestamp: nil
122
+ timestamp: nil,
123
+ origin: nil
113
124
  )
114
125
  @trace_id = trace_id || SecureRandom.uuid.delete("-")
115
126
  @span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16)
@@ -123,6 +134,7 @@ module Sentry
123
134
  @status = status
124
135
  @data = {}
125
136
  @tags = {}
137
+ @origin = origin || DEFAULT_SPAN_ORIGIN
126
138
  end
127
139
 
128
140
  # Finishes the span by adding a timestamp.
@@ -148,9 +160,15 @@ module Sentry
148
160
  transaction.get_baggage&.serialize
149
161
  end
150
162
 
163
+ # Returns the Dynamic Sampling Context from the transaction baggage.
164
+ # @return [Hash, nil]
165
+ def get_dynamic_sampling_context
166
+ transaction.get_baggage&.dynamic_sampling_context
167
+ end
168
+
151
169
  # @return [Hash]
152
170
  def to_hash
153
- {
171
+ hash = {
154
172
  trace_id: @trace_id,
155
173
  span_id: @span_id,
156
174
  parent_span_id: @parent_span_id,
@@ -160,8 +178,14 @@ module Sentry
160
178
  op: @op,
161
179
  status: @status,
162
180
  tags: @tags,
163
- data: @data
181
+ data: @data,
182
+ origin: @origin
164
183
  }
184
+
185
+ summary = metrics_summary
186
+ hash[:_metrics_summary] = summary if summary
187
+
188
+ hash
165
189
  end
166
190
 
167
191
  # Returns the span's context that can be used to embed in an Event.
@@ -173,7 +197,9 @@ module Sentry
173
197
  parent_span_id: @parent_span_id,
174
198
  description: @description,
175
199
  op: @op,
176
- status: @status
200
+ status: @status,
201
+ origin: @origin,
202
+ data: @data
177
203
  }
178
204
  end
179
205
 
@@ -269,5 +295,20 @@ module Sentry
269
295
  def set_tag(key, value)
270
296
  @tags[key] = value
271
297
  end
298
+
299
+ # Sets the origin of the span.
300
+ # @param origin [String]
301
+ def set_origin(origin)
302
+ @origin = origin
303
+ end
304
+
305
+ # Collects gauge metrics on the span for metric summaries.
306
+ def metrics_local_aggregator
307
+ @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new
308
+ end
309
+
310
+ def metrics_summary
311
+ @metrics_local_aggregator&.to_hash
312
+ end
272
313
  end
273
314
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module TestHelper
3
- DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42'
5
+ DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
4
6
 
5
7
  # Alters the existing SDK configuration with test-suitable options. Mainly:
6
8
  # - Sets a dummy DSN instead of `nil` or an actual DSN.
@@ -20,7 +22,7 @@ module Sentry
20
22
  # set transport to DummyTransport, so we can easily intercept the captured events
21
23
  dummy_config.transport.transport_class = Sentry::DummyTransport
22
24
  # make sure SDK allows sending under the current environment
23
- dummy_config.enabled_environments << dummy_config.environment unless dummy_config.enabled_environments.include?(dummy_config.environment)
25
+ dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment)
24
26
  # disble async event sending
25
27
  dummy_config.background_worker_threads = 0
26
28
 
@@ -50,6 +52,7 @@ module Sentry
50
52
  if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1
51
53
  Sentry.get_current_hub.pop_scope
52
54
  end
55
+ Sentry::Scope.global_event_processors.clear
53
56
  end
54
57
 
55
58
  # @return [Transport]
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class ThreadedPeriodicWorker
5
+ include LoggingHelper
6
+
7
+ def initialize(logger, internal)
8
+ @thread = nil
9
+ @exited = false
10
+ @interval = internal
11
+ @logger = logger
12
+ end
13
+
14
+ def ensure_thread
15
+ return false if @exited
16
+ return true if @thread&.alive?
17
+
18
+ @thread = Thread.new do
19
+ loop do
20
+ sleep(@interval)
21
+ run
22
+ end
23
+ end
24
+
25
+ true
26
+ rescue ThreadError
27
+ log_debug("[#{self.class.name}] thread creation failed")
28
+ @exited = true
29
+ false
30
+ end
31
+
32
+ def kill
33
+ log_debug("[#{self.class.name}] thread killed")
34
+
35
+ @exited = true
36
+ @thread&.kill
37
+ end
38
+ end
39
+ end
@@ -9,11 +9,11 @@ module Sentry
9
9
  # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
10
10
  SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
11
11
 
12
- UNLABELD_NAME = "<unlabeled transaction>".freeze
12
+ UNLABELD_NAME = "<unlabeled transaction>"
13
13
  MESSAGE_PREFIX = "[Tracing]"
14
14
 
15
15
  # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
16
- SOURCES = %i(custom url route view component task)
16
+ SOURCES = %i[custom url route view component task]
17
17
 
18
18
  include LoggingHelper
19
19
 
@@ -85,7 +85,7 @@ module Sentry
85
85
  @effective_sample_rate = nil
86
86
  @contexts = {}
87
87
  @measurements = {}
88
- @profiler = Profiler.new(@configuration)
88
+ @profiler = @configuration.profiler_class.new(@configuration)
89
89
  init_span_recorder
90
90
  end
91
91
 
@@ -110,14 +110,15 @@ module Sentry
110
110
 
111
111
  trace_id, parent_span_id, parent_sampled = sentry_trace_data
112
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
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
121
122
 
122
123
  baggage.freeze!
123
124
 
@@ -218,7 +219,12 @@ module Sentry
218
219
  if sample_rate == true
219
220
  @sampled = true
220
221
  else
221
- @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
222
228
  end
223
229
 
224
230
  if @sampled
@@ -257,7 +263,10 @@ module Sentry
257
263
  event = hub.current_client.event_from_transaction(self)
258
264
  hub.capture_event(event)
259
265
  else
260
- 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")
269
+ hub.current_client.transport.record_lost_event(reason, "span")
261
270
  end
262
271
  end
263
272
 
@@ -294,6 +303,11 @@ module Sentry
294
303
  profiler.start
295
304
  end
296
305
 
306
+ # These are high cardinality and thus bad
307
+ def source_low_quality?
308
+ source == :url
309
+ end
310
+
297
311
  protected
298
312
 
299
313
  def init_span_recorder(limit = 1000)
@@ -329,11 +343,6 @@ module Sentry
329
343
  @baggage = Baggage.new(items, mutable: false)
330
344
  end
331
345
 
332
- # These are high cardinality and thus bad
333
- def source_low_quality?
334
- source == :url
335
- end
336
-
337
346
  class SpanRecorder
338
347
  attr_reader :max_length, :spans
339
348
 
@@ -17,6 +17,9 @@ module Sentry
17
17
  # @return [Hash, nil]
18
18
  attr_accessor :profile
19
19
 
20
+ # @return [Hash, nil]
21
+ attr_accessor :metrics_summary
22
+
20
23
  def initialize(transaction:, **options)
21
24
  super(**options)
22
25
 
@@ -29,6 +32,7 @@ module Sentry
29
32
  self.tags = transaction.tags
30
33
  self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
31
34
  self.measurements = transaction.measurements
35
+ self.metrics_summary = transaction.metrics_summary
32
36
 
33
37
  finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
34
38
  self.spans = finished_spans.map(&:to_hash)
@@ -49,6 +53,7 @@ module Sentry
49
53
  data[:spans] = @spans.map(&:to_hash) if @spans
50
54
  data[:start_timestamp] = @start_timestamp
51
55
  data[:measurements] = @measurements
56
+ data[:_metrics_summary] = @metrics_summary if @metrics_summary
52
57
  data
53
58
  end
54
59
 
@@ -69,8 +74,7 @@ module Sentry
69
74
  id: event_id,
70
75
  name: transaction.name,
71
76
  trace_id: transaction.trace_id,
72
- # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
73
- active_thead_id: '0'
77
+ active_thread_id: transaction.profiler.active_thread_id.to_s
74
78
  }
75
79
  )
76
80
 
@@ -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
@@ -7,18 +7,26 @@ module Sentry
7
7
  class HTTPTransport < Transport
8
8
  GZIP_ENCODING = "gzip"
9
9
  GZIP_THRESHOLD = 1024 * 30
10
- CONTENT_TYPE = 'application/x-sentry-envelope'
10
+ CONTENT_TYPE = "application/x-sentry-envelope"
11
11
 
12
12
  DEFAULT_DELAY = 60
13
13
  RETRY_AFTER_HEADER = "retry-after"
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)
@@ -30,36 +38,78 @@ module Sentry
30
38
  end
31
39
 
32
40
  headers = {
33
- 'Content-Type' => CONTENT_TYPE,
34
- 'Content-Encoding' => encoding,
35
- 'X-Sentry-Auth' => generate_auth_header,
36
- 'User-Agent' => USER_AGENT
41
+ "Content-Type" => CONTENT_TYPE,
42
+ "Content-Encoding" => encoding,
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