sentry-ruby 5.9.0 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +2 -10
  3. data/README.md +9 -9
  4. data/lib/sentry/background_worker.rb +8 -1
  5. data/lib/sentry/backpressure_monitor.rb +75 -0
  6. data/lib/sentry/breadcrumb.rb +8 -2
  7. data/lib/sentry/check_in_event.rb +60 -0
  8. data/lib/sentry/client.rb +48 -10
  9. data/lib/sentry/configuration.rb +89 -17
  10. data/lib/sentry/cron/configuration.rb +23 -0
  11. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  12. data/lib/sentry/cron/monitor_config.rb +53 -0
  13. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  14. data/lib/sentry/envelope.rb +1 -1
  15. data/lib/sentry/event.rb +6 -28
  16. data/lib/sentry/hub.rb +74 -2
  17. data/lib/sentry/integrable.rb +6 -0
  18. data/lib/sentry/interfaces/single_exception.rb +5 -3
  19. data/lib/sentry/net/http.rb +26 -20
  20. data/lib/sentry/profiler.rb +18 -7
  21. data/lib/sentry/propagation_context.rb +134 -0
  22. data/lib/sentry/puma.rb +11 -4
  23. data/lib/sentry/rack/capture_exceptions.rb +1 -4
  24. data/lib/sentry/rake.rb +0 -13
  25. data/lib/sentry/redis.rb +9 -3
  26. data/lib/sentry/release_detector.rb +1 -1
  27. data/lib/sentry/scope.rb +29 -13
  28. data/lib/sentry/span.rb +39 -2
  29. data/lib/sentry/test_helper.rb +18 -12
  30. data/lib/sentry/transaction.rb +18 -19
  31. data/lib/sentry/transaction_event.rb +0 -3
  32. data/lib/sentry/transport/configuration.rb +74 -1
  33. data/lib/sentry/transport/http_transport.rb +68 -37
  34. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  35. data/lib/sentry/transport.rb +21 -17
  36. data/lib/sentry/utils/argument_checking_helper.rb +9 -3
  37. data/lib/sentry/version.rb +1 -1
  38. data/lib/sentry-ruby.rb +83 -25
  39. metadata +10 -3
  40. data/CODE_OF_CONDUCT.md +0 -74
data/lib/sentry/puma.rb CHANGED
@@ -1,10 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ return unless defined?(Puma::Server)
4
+
3
5
  module Sentry
4
6
  module Puma
5
7
  module Server
8
+ PUMA_4_AND_PRIOR = Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("5.0.0")
9
+
6
10
  def lowlevel_error(e, env, status=500)
7
- result = super
11
+ result =
12
+ if PUMA_4_AND_PRIOR
13
+ super(e, env)
14
+ else
15
+ super
16
+ end
8
17
 
9
18
  begin
10
19
  Sentry.capture_exception(e) do |scope|
@@ -20,6 +29,4 @@ module Sentry
20
29
  end
21
30
  end
22
31
 
23
- if defined?(Puma::Server)
24
- Sentry.register_patch(Sentry::Puma::Server, Puma::Server)
25
- end
32
+ Sentry.register_patch(:puma, Sentry::Puma::Server, Puma::Server)
@@ -62,11 +62,8 @@ module Sentry
62
62
  end
63
63
 
64
64
  def start_transaction(env, scope)
65
- sentry_trace = env["HTTP_SENTRY_TRACE"]
66
- baggage = env["HTTP_BAGGAGE"]
67
-
68
65
  options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
69
- transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
66
+ transaction = Sentry.continue_trace(env, **options)
70
67
  Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
71
68
  end
72
69
 
data/lib/sentry/rake.rb CHANGED
@@ -17,15 +17,6 @@ module Sentry
17
17
  super
18
18
  end
19
19
  end
20
-
21
- module Task
22
- # @api private
23
- def execute(args=nil)
24
- return super unless Sentry.initialized? && Sentry.get_current_hub
25
-
26
- super
27
- end
28
- end
29
20
  end
30
21
  end
31
22
 
@@ -34,8 +25,4 @@ module Rake
34
25
  class Application
35
26
  prepend(Sentry::Rake::Application)
36
27
  end
37
-
38
- class Task
39
- prepend(Sentry::Rake::Task)
40
- end
41
28
  end
data/lib/sentry/redis.rb CHANGED
@@ -19,7 +19,10 @@ module Sentry
19
19
 
20
20
  if span
21
21
  span.set_description(commands_description)
22
- span.set_data(:server, server_description)
22
+ span.set_data(Span::DataConventions::DB_SYSTEM, "redis")
23
+ span.set_data(Span::DataConventions::DB_NAME, db)
24
+ span.set_data(Span::DataConventions::SERVER_ADDRESS, host)
25
+ span.set_data(Span::DataConventions::SERVER_PORT, port)
23
26
  end
24
27
  end
25
28
  end
@@ -30,6 +33,7 @@ module Sentry
30
33
  attr_reader :commands, :host, :port, :db
31
34
 
32
35
  def record_breadcrumb
36
+ return unless Sentry.initialized?
33
37
  return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
34
38
 
35
39
  Sentry.add_breadcrumb(
@@ -95,8 +99,10 @@ end
95
99
 
96
100
  if defined?(::Redis::Client)
97
101
  if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
98
- Sentry.register_patch(Sentry::Redis::OldClientPatch, ::Redis::Client)
102
+ Sentry.register_patch(:redis, Sentry::Redis::OldClientPatch, ::Redis::Client)
99
103
  elsif defined?(RedisClient)
100
- RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
104
+ Sentry.register_patch(:redis) do
105
+ RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
106
+ end
101
107
  end
102
108
  end
@@ -28,7 +28,7 @@ module Sentry
28
28
  end
29
29
 
30
30
  def detect_release_from_git
31
- Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
31
+ Sentry.sys_command("git rev-parse HEAD") if File.directory?(".git")
32
32
  end
33
33
 
34
34
  def detect_release_from_env
data/lib/sentry/scope.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sentry/breadcrumb_buffer"
4
+ require "sentry/propagation_context"
4
5
  require "etc"
5
6
 
6
7
  module Sentry
@@ -20,7 +21,8 @@ module Sentry
20
21
  :event_processors,
21
22
  :rack_env,
22
23
  :span,
23
- :session
24
+ :session,
25
+ :propagation_context
24
26
  ]
25
27
 
26
28
  attr_reader(*ATTRIBUTES)
@@ -42,22 +44,26 @@ module Sentry
42
44
  # @param hint [Hash] the hint data that'll be passed to event processors.
43
45
  # @return [Event]
44
46
  def apply_to_event(event, hint = nil)
45
- event.tags = tags.merge(event.tags)
46
- event.user = user.merge(event.user)
47
- event.extra = extra.merge(event.extra)
48
- event.contexts = contexts.merge(event.contexts)
49
- event.transaction = transaction_name if transaction_name
50
- event.transaction_info = { source: transaction_source } if transaction_source
47
+ unless event.is_a?(CheckInEvent)
48
+ event.tags = tags.merge(event.tags)
49
+ event.user = user.merge(event.user)
50
+ event.extra = extra.merge(event.extra)
51
+ event.contexts = contexts.merge(event.contexts)
52
+ event.transaction = transaction_name if transaction_name
53
+ event.transaction_info = { source: transaction_source } if transaction_source
54
+ event.fingerprint = fingerprint
55
+ event.level = level
56
+ event.breadcrumbs = breadcrumbs
57
+ event.rack_env = rack_env if rack_env
58
+ end
51
59
 
52
60
  if span
53
- event.contexts[:trace] = span.get_trace_context
61
+ event.contexts[:trace] ||= span.get_trace_context
62
+ else
63
+ event.contexts[:trace] ||= propagation_context.get_trace_context
64
+ event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
54
65
  end
55
66
 
56
- event.fingerprint = fingerprint
57
- event.level = level
58
- event.breadcrumbs = breadcrumbs
59
- event.rack_env = rack_env if rack_env
60
-
61
67
  all_event_processors = self.class.global_event_processors + @event_processors
62
68
 
63
69
  unless all_event_processors.empty?
@@ -95,6 +101,7 @@ module Sentry
95
101
  copy.fingerprint = fingerprint.deep_dup
96
102
  copy.span = span.deep_dup
97
103
  copy.session = session.deep_dup
104
+ copy.propagation_context = propagation_context.deep_dup
98
105
  copy
99
106
  end
100
107
 
@@ -111,6 +118,7 @@ module Sentry
111
118
  self.transaction_sources = scope.transaction_sources
112
119
  self.fingerprint = scope.fingerprint
113
120
  self.span = scope.span
121
+ self.propagation_context = scope.propagation_context
114
122
  end
115
123
 
116
124
  # Updates the scope's data from the given options.
@@ -272,6 +280,13 @@ module Sentry
272
280
  @event_processors << block
273
281
  end
274
282
 
283
+ # Generate a new propagation context either from the incoming env headers or from scratch.
284
+ # @param env [Hash, nil]
285
+ # @return [void]
286
+ def generate_propagation_context(env = nil)
287
+ @propagation_context = PropagationContext.new(self, env)
288
+ end
289
+
275
290
  protected
276
291
 
277
292
  # for duplicating scopes internally
@@ -292,6 +307,7 @@ module Sentry
292
307
  @rack_env = {}
293
308
  @span = nil
294
309
  @session = nil
310
+ generate_propagation_context
295
311
  set_new_breadcrumb_buffer
296
312
  end
297
313
 
data/lib/sentry/span.rb CHANGED
@@ -4,6 +4,43 @@ require "securerandom"
4
4
 
5
5
  module Sentry
6
6
  class Span
7
+
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
@@ -208,7 +245,7 @@ module Sentry
208
245
  # @param status_code [String] example: "500".
209
246
  def set_http_status(status_code)
210
247
  status_code = status_code.to_i
211
- set_data("status_code", status_code)
248
+ set_data(DataConventions::HTTP_STATUS_CODE, status_code)
212
249
 
213
250
  status =
214
251
  if status_code >= 200 && status_code < 299
@@ -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,16 +2,13 @@
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
 
@@ -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`.
@@ -132,18 +131,10 @@ module Sentry
132
131
  )
133
132
  end
134
133
 
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.
134
+ # @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead.
138
135
  # @return [Array, nil]
139
136
  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]
137
+ PropagationContext.extract_sentry_trace(sentry_trace)
147
138
  end
148
139
 
149
140
  # @return [Hash]
@@ -227,7 +218,12 @@ module Sentry
227
218
  if sample_rate == true
228
219
  @sampled = true
229
220
  else
230
- @sampled = Random.rand < sample_rate
221
+ if Sentry.backpressure_monitor
222
+ factor = Sentry.backpressure_monitor.downsample_factor
223
+ @effective_sample_rate /= 2**factor
224
+ end
225
+
226
+ @sampled = Random.rand < @effective_sample_rate
231
227
  end
232
228
 
233
229
  if @sampled
@@ -266,7 +262,9 @@ module Sentry
266
262
  event = hub.current_client.event_from_transaction(self)
267
263
  hub.capture_event(event)
268
264
  else
269
- hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
265
+ is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive?
266
+ reason = is_backpressure ? :backpressure : :sample_rate
267
+ hub.current_client.transport.record_lost_event(reason, 'transaction')
270
268
  end
271
269
  end
272
270
 
@@ -323,6 +321,7 @@ module Sentry
323
321
  items = {
324
322
  "trace_id" => trace_id,
325
323
  "sample_rate" => effective_sample_rate&.to_s,
324
+ "sampled" => sampled&.to_s,
326
325
  "environment" => @environment,
327
326
  "release" => @release,
328
327
  "public_key" => @dsn&.public_key
@@ -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
 
@@ -3,7 +3,80 @@
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
+
7
+ # The timeout in seconds to open a connection to Sentry, in seconds.
8
+ # Default value is 2.
9
+ #
10
+ # @return [Integer]
11
+ attr_accessor :timeout
12
+
13
+ # The timeout in seconds to read data from Sentry, in seconds.
14
+ # Default value is 1.
15
+ #
16
+ # @return [Integer]
17
+ attr_accessor :open_timeout
18
+
19
+ # The proxy configuration to use to connect to Sentry.
20
+ # Accepts either a URI formatted string, URI, or a hash with the `uri`,
21
+ # `user`, and `password` keys.
22
+ #
23
+ # @example
24
+ # # setup proxy using a string:
25
+ # config.transport.proxy = "https://user:password@proxyhost:8080"
26
+ #
27
+ # # setup proxy using a URI:
28
+ # config.transport.proxy = URI("https://user:password@proxyhost:8080")
29
+ #
30
+ # # setup proxy using a hash:
31
+ # config.transport.proxy = {
32
+ # uri: URI("https://proxyhost:8080"),
33
+ # user: "user",
34
+ # password: "password"
35
+ # }
36
+ #
37
+ # If you're using the default transport (`Sentry::HTTPTransport`),
38
+ # proxy settings will also automatically be read from tne environment
39
+ # variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`).
40
+ #
41
+ # @return [String, URI, Hash, nil]
42
+ attr_accessor :proxy
43
+
44
+ # The SSL configuration to use to connect to Sentry.
45
+ # You can either pass a `Hash` containing `ca_file` and `verification` keys,
46
+ # or you can set those options directly on the `Sentry::HTTPTransport::Configuration` object:
47
+ #
48
+ # @example
49
+ # config.transport.ssl = {
50
+ # ca_file: "/path/to/ca_file",
51
+ # verification: true
52
+ # end
53
+ #
54
+ # @return [Hash, nil]
55
+ attr_accessor :ssl
56
+
57
+ # The path to the CA file to use to verify the SSL connection.
58
+ # Default value is `nil`.
59
+ #
60
+ # @return [String, nil]
61
+ attr_accessor :ssl_ca_file
62
+
63
+ # Whether to verify that the peer certificate is valid in SSL connections.
64
+ # Default value is `true`.
65
+ #
66
+ # @return [Boolean]
67
+ attr_accessor :ssl_verification
68
+
69
+ # The encoding to use to compress the request body.
70
+ # Default value is `Sentry::HTTPTransport::GZIP_ENCODING`.
71
+ #
72
+ # @return [String]
73
+ attr_accessor :encoding
74
+
75
+ # The class to use as a transport to connect to Sentry.
76
+ # If this option not set, it will return `nil`, and Sentry will use
77
+ # `Sentry::HTTPTransport` by default.
78
+ #
79
+ # @return [Class, nil]
7
80
  attr_reader :transport_class
8
81
 
9
82
  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