sentry-ruby 5.13.0 → 5.21.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 (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