sentry-ruby 5.10.0 → 5.17.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +2 -13
- data/README.md +10 -10
- data/Rakefile +1 -1
- data/lib/sentry/background_worker.rb +9 -2
- data/lib/sentry/backpressure_monitor.rb +75 -0
- data/lib/sentry/backtrace.rb +7 -3
- data/lib/sentry/breadcrumb.rb +8 -2
- data/lib/sentry/check_in_event.rb +60 -0
- data/lib/sentry/client.rb +88 -17
- data/lib/sentry/configuration.rb +66 -12
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +75 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +1 -1
- data/lib/sentry/envelope.rb +19 -2
- data/lib/sentry/error_event.rb +2 -2
- data/lib/sentry/event.rb +14 -36
- data/lib/sentry/hub.rb +70 -2
- data/lib/sentry/integrable.rb +10 -0
- data/lib/sentry/interface.rb +1 -0
- data/lib/sentry/interfaces/exception.rb +5 -3
- data/lib/sentry/interfaces/mechanism.rb +20 -0
- data/lib/sentry/interfaces/request.rb +2 -2
- data/lib/sentry/interfaces/single_exception.rb +10 -6
- data/lib/sentry/interfaces/stacktrace_builder.rb +8 -0
- data/lib/sentry/metrics/aggregator.rb +276 -0
- data/lib/sentry/metrics/configuration.rb +47 -0
- data/lib/sentry/metrics/counter_metric.rb +25 -0
- data/lib/sentry/metrics/distribution_metric.rb +25 -0
- data/lib/sentry/metrics/gauge_metric.rb +35 -0
- data/lib/sentry/metrics/local_aggregator.rb +53 -0
- data/lib/sentry/metrics/metric.rb +19 -0
- data/lib/sentry/metrics/set_metric.rb +28 -0
- data/lib/sentry/metrics/timing.rb +43 -0
- data/lib/sentry/metrics.rb +55 -0
- data/lib/sentry/net/http.rb +25 -22
- data/lib/sentry/profiler.rb +18 -7
- data/lib/sentry/propagation_context.rb +135 -0
- data/lib/sentry/puma.rb +12 -5
- data/lib/sentry/rack/capture_exceptions.rb +7 -5
- data/lib/sentry/rake.rb +3 -14
- data/lib/sentry/redis.rb +8 -3
- data/lib/sentry/release_detector.rb +1 -1
- data/lib/sentry/scope.rb +36 -15
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +1 -6
- data/lib/sentry/span.rb +54 -3
- data/lib/sentry/test_helper.rb +18 -12
- data/lib/sentry/transaction.rb +33 -33
- data/lib/sentry/transaction_event.rb +5 -3
- data/lib/sentry/transport/configuration.rb +73 -1
- data/lib/sentry/transport/http_transport.rb +68 -37
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +27 -37
- data/lib/sentry/utils/argument_checking_helper.rb +12 -0
- data/lib/sentry/utils/real_ip.rb +1 -1
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +96 -26
- data/sentry-ruby.gemspec +1 -0
- 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.
|
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(
|
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
|
data/lib/sentry/test_helper.rb
CHANGED
@@ -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
|
-
|
17
|
+
dummy_config = Sentry.configuration.dup
|
18
18
|
# configure dummy DSN, so the events will not be sent to the actual service
|
19
|
-
|
19
|
+
dummy_config.dsn = DUMMY_DSN
|
20
20
|
# set transport to DummyTransport, so we can easily intercept the captured events
|
21
|
-
|
21
|
+
dummy_config.transport.transport_class = Sentry::DummyTransport
|
22
22
|
# make sure SDK allows sending under the current environment
|
23
|
-
|
23
|
+
dummy_config.enabled_environments << dummy_config.environment unless dummy_config.enabled_environments.include?(dummy_config.environment)
|
24
24
|
# disble async event sending
|
25
|
-
|
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(
|
30
|
+
block&.call(dummy_config)
|
31
31
|
|
32
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
data/lib/sentry/transaction.rb
CHANGED
@@ -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
|
9
|
-
|
10
|
-
|
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
|
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 =
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
@
|
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(
|
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
|
-
|
48
|
-
|
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
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
130
|
-
|
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
|