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.
- checksums.yaml +4 -4
- data/Gemfile +7 -18
- data/README.md +20 -10
- data/Rakefile +3 -1
- data/bin/console +2 -0
- data/lib/sentry/attachment.rb +40 -0
- data/lib/sentry/background_worker.rb +9 -2
- data/lib/sentry/backpressure_monitor.rb +45 -0
- data/lib/sentry/backtrace.rb +10 -8
- data/lib/sentry/baggage.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
- data/lib/sentry/check_in_event.rb +5 -5
- data/lib/sentry/client.rb +71 -18
- data/lib/sentry/configuration.rb +108 -32
- data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +42 -26
- data/lib/sentry/cron/monitor_config.rb +1 -1
- data/lib/sentry/cron/monitor_schedule.rb +1 -1
- data/lib/sentry/dsn.rb +4 -4
- data/lib/sentry/envelope/item.rb +88 -0
- data/lib/sentry/envelope.rb +2 -68
- data/lib/sentry/error_event.rb +2 -2
- data/lib/sentry/event.rb +20 -46
- data/lib/sentry/faraday.rb +77 -0
- data/lib/sentry/graphql.rb +9 -0
- data/lib/sentry/hub.rb +25 -5
- data/lib/sentry/integrable.rb +4 -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 +7 -7
- data/lib/sentry/interfaces/single_exception.rb +10 -7
- data/lib/sentry/interfaces/stacktrace.rb +3 -1
- data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
- data/lib/sentry/logger.rb +1 -1
- data/lib/sentry/metrics/aggregator.rb +248 -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 +56 -0
- data/lib/sentry/net/http.rb +22 -39
- data/lib/sentry/profiler/helpers.rb +46 -0
- data/lib/sentry/profiler.rb +25 -56
- data/lib/sentry/propagation_context.rb +10 -9
- data/lib/sentry/puma.rb +1 -1
- data/lib/sentry/rack/capture_exceptions.rb +16 -4
- data/lib/sentry/rack.rb +2 -2
- data/lib/sentry/rake.rb +4 -15
- data/lib/sentry/redis.rb +2 -1
- data/lib/sentry/release_detector.rb +5 -5
- data/lib/sentry/scope.rb +48 -37
- data/lib/sentry/session.rb +2 -2
- data/lib/sentry/session_flusher.rb +7 -39
- data/lib/sentry/span.rb +46 -5
- data/lib/sentry/test_helper.rb +5 -2
- data/lib/sentry/threaded_periodic_worker.rb +39 -0
- data/lib/sentry/transaction.rb +27 -18
- data/lib/sentry/transaction_event.rb +6 -2
- data/lib/sentry/transport/configuration.rb +73 -1
- data/lib/sentry/transport/http_transport.rb +72 -41
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +36 -41
- data/lib/sentry/utils/argument_checking_helper.rb +6 -0
- data/lib/sentry/utils/env_helper.rb +21 -0
- data/lib/sentry/utils/http_tracing.rb +41 -0
- data/lib/sentry/utils/logging_helper.rb +0 -4
- data/lib/sentry/utils/real_ip.rb +2 -2
- data/lib/sentry/utils/request_id.rb +1 -1
- data/lib/sentry/vernier/output.rb +89 -0
- data/lib/sentry/vernier/profiler.rb +125 -0
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +61 -27
- data/sentry-ruby-core.gemspec +3 -1
- data/sentry-ruby.gemspec +15 -6
- 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
|
-
|
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
|
-
|
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:
|
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
|
data/lib/sentry/test_helper.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Sentry
|
2
4
|
module TestHelper
|
3
|
-
DUMMY_DSN =
|
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
|
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
|
data/lib/sentry/transaction.rb
CHANGED
@@ -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>"
|
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
|
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 =
|
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 =
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
@
|
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
|
-
|
34
|
-
|
35
|
-
|
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(
|
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
|
|