sentry-ruby 5.3.0 → 5.8.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/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +31 -0
- data/Makefile +4 -0
- data/README.md +10 -6
- data/Rakefile +13 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +72 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +81 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +70 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/client.rb +207 -0
- data/lib/sentry/configuration.rb +543 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +96 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +178 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +241 -0
- data/lib/sentry/integrable.rb +26 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +65 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +103 -0
- data/lib/sentry/rack/capture_exceptions.rb +82 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +41 -0
- data/lib/sentry/redis.rb +107 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +339 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +236 -0
- data/lib/sentry/test_helper.rb +78 -0
- data/lib/sentry/transaction.rb +345 -0
- data/lib/sentry/transaction_event.rb +53 -0
- data/lib/sentry/transport/configuration.rb +25 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +175 -0
- data/lib/sentry/transport.rb +214 -0
- data/lib/sentry/utils/argument_checking_helper.rb +13 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +511 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +66 -16
@@ -0,0 +1,78 @@
|
|
1
|
+
module Sentry
|
2
|
+
module TestHelper
|
3
|
+
DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42'
|
4
|
+
|
5
|
+
# Alters the existing SDK configuration with test-suitable options. Mainly:
|
6
|
+
# - Sets a dummy DSN instead of `nil` or an actual DSN.
|
7
|
+
# - Sets the transport to DummyTransport, which allows easy access to the captured events.
|
8
|
+
# - Disables background worker.
|
9
|
+
# - Makes sure the SDK is enabled under the current environment ("test" in most cases).
|
10
|
+
#
|
11
|
+
# It should be called **before** every test case.
|
12
|
+
#
|
13
|
+
# @yieldparam config [Configuration]
|
14
|
+
# @return [void]
|
15
|
+
def setup_sentry_test(&block)
|
16
|
+
raise "please make sure the SDK is initialized for testing" unless Sentry.initialized?
|
17
|
+
copied_config = Sentry.configuration.dup
|
18
|
+
# configure dummy DSN, so the events will not be sent to the actual service
|
19
|
+
copied_config.dsn = DUMMY_DSN
|
20
|
+
# set transport to DummyTransport, so we can easily intercept the captured events
|
21
|
+
copied_config.transport.transport_class = Sentry::DummyTransport
|
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)
|
24
|
+
# disble async event sending
|
25
|
+
copied_config.background_worker_threads = 0
|
26
|
+
|
27
|
+
# user can overwrite some of the configs, with a few exceptions like:
|
28
|
+
# - include_local_variables
|
29
|
+
# - auto_session_tracking
|
30
|
+
block&.call(copied_config)
|
31
|
+
|
32
|
+
test_client = Sentry::Client.new(copied_config)
|
33
|
+
Sentry.get_current_hub.bind_client(test_client)
|
34
|
+
Sentry.get_current_scope.clear
|
35
|
+
end
|
36
|
+
|
37
|
+
# Clears all stored events and envelopes.
|
38
|
+
# It should be called **after** every test case.
|
39
|
+
# @return [void]
|
40
|
+
def teardown_sentry_test
|
41
|
+
return unless Sentry.initialized?
|
42
|
+
|
43
|
+
sentry_transport.events = []
|
44
|
+
sentry_transport.envelopes = []
|
45
|
+
Sentry.get_current_scope.clear
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Transport]
|
49
|
+
def sentry_transport
|
50
|
+
Sentry.get_current_client.transport
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the captured event objects.
|
54
|
+
# @return [Array<Event>]
|
55
|
+
def sentry_events
|
56
|
+
sentry_transport.events
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the captured envelope objects.
|
60
|
+
# @return [Array<Envelope>]
|
61
|
+
def sentry_envelopes
|
62
|
+
sentry_transport.envelopes
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the last captured event object.
|
66
|
+
# @return [Event, nil]
|
67
|
+
def last_sentry_event
|
68
|
+
sentry_events.last
|
69
|
+
end
|
70
|
+
|
71
|
+
# Extracts SDK's internal exception container (not actual exception objects) from an given event.
|
72
|
+
# @return [Array<Sentry::SingleExceptionInterface>]
|
73
|
+
def extract_sentry_exceptions(event)
|
74
|
+
event&.exception&.values || []
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
@@ -0,0 +1,345 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sentry/baggage"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Transaction < Span
|
7
|
+
SENTRY_TRACE_REGEXP = Regexp.new(
|
8
|
+
"^[ \t]*" + # whitespace
|
9
|
+
"([0-9a-f]{32})?" + # trace_id
|
10
|
+
"-?([0-9a-f]{16})?" + # span_id
|
11
|
+
"-?([01])?" + # sampled
|
12
|
+
"[ \t]*$" # whitespace
|
13
|
+
)
|
14
|
+
UNLABELD_NAME = "<unlabeled transaction>".freeze
|
15
|
+
MESSAGE_PREFIX = "[Tracing]"
|
16
|
+
|
17
|
+
# https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
|
18
|
+
SOURCES = %i(custom url route view component task)
|
19
|
+
|
20
|
+
include LoggingHelper
|
21
|
+
|
22
|
+
# The name of the transaction.
|
23
|
+
# @return [String]
|
24
|
+
attr_reader :name
|
25
|
+
|
26
|
+
# The source of the transaction name.
|
27
|
+
# @return [Symbol]
|
28
|
+
attr_reader :source
|
29
|
+
|
30
|
+
# The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
|
31
|
+
# @return [String]
|
32
|
+
attr_reader :parent_sampled
|
33
|
+
|
34
|
+
# The parsed incoming W3C baggage header.
|
35
|
+
# This is only for accessing the current baggage variable.
|
36
|
+
# Please use the #get_baggage method for interfacing outside this class.
|
37
|
+
# @return [Baggage, nil]
|
38
|
+
attr_reader :baggage
|
39
|
+
|
40
|
+
# The measurements added to the transaction.
|
41
|
+
# @return [Hash]
|
42
|
+
attr_reader :measurements
|
43
|
+
|
44
|
+
# @deprecated Use Sentry.get_current_hub instead.
|
45
|
+
attr_reader :hub
|
46
|
+
|
47
|
+
# @deprecated Use Sentry.configuration instead.
|
48
|
+
attr_reader :configuration
|
49
|
+
|
50
|
+
# @deprecated Use Sentry.logger instead.
|
51
|
+
attr_reader :logger
|
52
|
+
|
53
|
+
# The effective sample rate at which this transaction was sampled.
|
54
|
+
# @return [Float, nil]
|
55
|
+
attr_reader :effective_sample_rate
|
56
|
+
|
57
|
+
# Additional contexts stored directly on the transaction object.
|
58
|
+
# @return [Hash]
|
59
|
+
attr_reader :contexts
|
60
|
+
|
61
|
+
def initialize(
|
62
|
+
hub:,
|
63
|
+
name: nil,
|
64
|
+
source: :custom,
|
65
|
+
parent_sampled: nil,
|
66
|
+
baggage: nil,
|
67
|
+
**options
|
68
|
+
)
|
69
|
+
super(transaction: self, **options)
|
70
|
+
|
71
|
+
set_name(name, source: source)
|
72
|
+
@parent_sampled = parent_sampled
|
73
|
+
@hub = hub
|
74
|
+
@baggage = baggage
|
75
|
+
@configuration = hub.configuration # to be removed
|
76
|
+
@tracing_enabled = hub.configuration.tracing_enabled?
|
77
|
+
@traces_sampler = hub.configuration.traces_sampler
|
78
|
+
@traces_sample_rate = hub.configuration.traces_sample_rate
|
79
|
+
@logger = hub.configuration.logger
|
80
|
+
@release = hub.configuration.release
|
81
|
+
@environment = hub.configuration.environment
|
82
|
+
@dsn = hub.configuration.dsn
|
83
|
+
@effective_sample_rate = nil
|
84
|
+
@contexts = {}
|
85
|
+
@measurements = {}
|
86
|
+
init_span_recorder
|
87
|
+
end
|
88
|
+
|
89
|
+
# Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
|
90
|
+
#
|
91
|
+
# The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
|
92
|
+
#
|
93
|
+
# The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
|
94
|
+
# @param sentry_trace [String] the trace string from the previous transaction.
|
95
|
+
# @param baggage [String, nil] the incoming baggage header string.
|
96
|
+
# @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
|
97
|
+
# @param options [Hash] the options you want to use to initialize a Transaction instance.
|
98
|
+
# @return [Transaction, nil]
|
99
|
+
def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options)
|
100
|
+
return unless hub.configuration.tracing_enabled?
|
101
|
+
return unless sentry_trace
|
102
|
+
|
103
|
+
sentry_trace_data = extract_sentry_trace(sentry_trace)
|
104
|
+
return unless sentry_trace_data
|
105
|
+
|
106
|
+
trace_id, parent_span_id, parent_sampled = sentry_trace_data
|
107
|
+
|
108
|
+
baggage = if baggage && !baggage.empty?
|
109
|
+
Baggage.from_incoming_header(baggage)
|
110
|
+
else
|
111
|
+
# If there's an incoming sentry-trace but no incoming baggage header,
|
112
|
+
# for instance in traces coming from older SDKs,
|
113
|
+
# baggage will be empty and frozen and won't be populated as head SDK.
|
114
|
+
Baggage.new({})
|
115
|
+
end
|
116
|
+
|
117
|
+
baggage.freeze!
|
118
|
+
|
119
|
+
new(
|
120
|
+
trace_id: trace_id,
|
121
|
+
parent_span_id: parent_span_id,
|
122
|
+
parent_sampled: parent_sampled,
|
123
|
+
hub: hub,
|
124
|
+
baggage: baggage,
|
125
|
+
**options
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
|
130
|
+
#
|
131
|
+
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
|
132
|
+
# @return [Array, nil]
|
133
|
+
def self.extract_sentry_trace(sentry_trace)
|
134
|
+
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
|
135
|
+
return nil if match.nil?
|
136
|
+
|
137
|
+
trace_id, parent_span_id, sampled_flag = match[1..3]
|
138
|
+
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
|
139
|
+
|
140
|
+
[trace_id, parent_span_id, parent_sampled]
|
141
|
+
end
|
142
|
+
|
143
|
+
# @return [Hash]
|
144
|
+
def to_hash
|
145
|
+
hash = super
|
146
|
+
|
147
|
+
hash.merge!(
|
148
|
+
name: @name,
|
149
|
+
source: @source,
|
150
|
+
sampled: @sampled,
|
151
|
+
parent_sampled: @parent_sampled
|
152
|
+
)
|
153
|
+
|
154
|
+
hash
|
155
|
+
end
|
156
|
+
|
157
|
+
# @return [Transaction]
|
158
|
+
def deep_dup
|
159
|
+
copy = super
|
160
|
+
copy.init_span_recorder(@span_recorder.max_length)
|
161
|
+
|
162
|
+
@span_recorder.spans.each do |span|
|
163
|
+
# span_recorder's first span is the current span, which should not be added to the copy's spans
|
164
|
+
next if span == self
|
165
|
+
copy.span_recorder.add(span.dup)
|
166
|
+
end
|
167
|
+
|
168
|
+
copy
|
169
|
+
end
|
170
|
+
|
171
|
+
# Sets a custom measurement on the transaction.
|
172
|
+
# @param name [String] name of the measurement
|
173
|
+
# @param value [Float] value of the measurement
|
174
|
+
# @param unit [String] unit of the measurement
|
175
|
+
# @return [void]
|
176
|
+
def set_measurement(name, value, unit = "")
|
177
|
+
@measurements[name] = { value: value, unit: unit }
|
178
|
+
end
|
179
|
+
|
180
|
+
# Sets initial sampling decision of the transaction.
|
181
|
+
# @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
|
182
|
+
# @return [void]
|
183
|
+
def set_initial_sample_decision(sampling_context:)
|
184
|
+
unless @tracing_enabled
|
185
|
+
@sampled = false
|
186
|
+
return
|
187
|
+
end
|
188
|
+
|
189
|
+
unless @sampled.nil?
|
190
|
+
@effective_sample_rate = @sampled ? 1.0 : 0.0
|
191
|
+
return
|
192
|
+
end
|
193
|
+
|
194
|
+
sample_rate =
|
195
|
+
if @traces_sampler.is_a?(Proc)
|
196
|
+
@traces_sampler.call(sampling_context)
|
197
|
+
elsif !sampling_context[:parent_sampled].nil?
|
198
|
+
sampling_context[:parent_sampled]
|
199
|
+
else
|
200
|
+
@traces_sample_rate
|
201
|
+
end
|
202
|
+
|
203
|
+
transaction_description = generate_transaction_description
|
204
|
+
|
205
|
+
if [true, false].include?(sample_rate)
|
206
|
+
@effective_sample_rate = sample_rate ? 1.0 : 0.0
|
207
|
+
elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0
|
208
|
+
@effective_sample_rate = sample_rate.to_f
|
209
|
+
else
|
210
|
+
@sampled = false
|
211
|
+
log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
|
212
|
+
return
|
213
|
+
end
|
214
|
+
|
215
|
+
if sample_rate == 0.0 || sample_rate == false
|
216
|
+
@sampled = false
|
217
|
+
log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
|
218
|
+
return
|
219
|
+
end
|
220
|
+
|
221
|
+
if sample_rate == true
|
222
|
+
@sampled = true
|
223
|
+
else
|
224
|
+
@sampled = Random.rand < sample_rate
|
225
|
+
end
|
226
|
+
|
227
|
+
if @sampled
|
228
|
+
log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
|
229
|
+
else
|
230
|
+
log_debug(
|
231
|
+
"#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
|
232
|
+
)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Finishes the transaction's recording and send it to Sentry.
|
237
|
+
# @param hub [Hub] the hub that'll send this transaction. (Deprecated)
|
238
|
+
# @return [TransactionEvent]
|
239
|
+
def finish(hub: nil, end_timestamp: nil)
|
240
|
+
if hub
|
241
|
+
log_warn(
|
242
|
+
<<~MSG
|
243
|
+
Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
|
244
|
+
Please use `Hub#start_transaction` with the designated hub.
|
245
|
+
MSG
|
246
|
+
)
|
247
|
+
end
|
248
|
+
|
249
|
+
hub ||= @hub
|
250
|
+
|
251
|
+
super(end_timestamp: end_timestamp)
|
252
|
+
|
253
|
+
if @name.nil?
|
254
|
+
@name = UNLABELD_NAME
|
255
|
+
end
|
256
|
+
|
257
|
+
if @sampled
|
258
|
+
event = hub.current_client.event_from_transaction(self)
|
259
|
+
hub.capture_event(event)
|
260
|
+
else
|
261
|
+
hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Get the existing frozen incoming baggage
|
266
|
+
# or populate one with sentry- items as the head SDK.
|
267
|
+
# @return [Baggage]
|
268
|
+
def get_baggage
|
269
|
+
populate_head_baggage if @baggage.nil? || @baggage.mutable
|
270
|
+
@baggage
|
271
|
+
end
|
272
|
+
|
273
|
+
# Set the transaction name directly.
|
274
|
+
# Considered internal api since it bypasses the usual scope logic.
|
275
|
+
# @param name [String]
|
276
|
+
# @param source [Symbol]
|
277
|
+
# @return [void]
|
278
|
+
def set_name(name, source: :custom)
|
279
|
+
@name = name
|
280
|
+
@source = SOURCES.include?(source) ? source.to_sym : :custom
|
281
|
+
end
|
282
|
+
|
283
|
+
# Set contexts directly on the transaction.
|
284
|
+
# @param key [String, Symbol]
|
285
|
+
# @param value [Object]
|
286
|
+
# @return [void]
|
287
|
+
def set_context(key, value)
|
288
|
+
@contexts[key] = value
|
289
|
+
end
|
290
|
+
|
291
|
+
protected
|
292
|
+
|
293
|
+
def init_span_recorder(limit = 1000)
|
294
|
+
@span_recorder = SpanRecorder.new(limit)
|
295
|
+
@span_recorder.add(self)
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
|
300
|
+
def generate_transaction_description
|
301
|
+
result = op.nil? ? "" : "<#{@op}> "
|
302
|
+
result += "transaction"
|
303
|
+
result += " <#{@name}>" if @name
|
304
|
+
result
|
305
|
+
end
|
306
|
+
|
307
|
+
def populate_head_baggage
|
308
|
+
items = {
|
309
|
+
"trace_id" => trace_id,
|
310
|
+
"sample_rate" => effective_sample_rate&.to_s,
|
311
|
+
"environment" => @environment,
|
312
|
+
"release" => @release,
|
313
|
+
"public_key" => @dsn&.public_key
|
314
|
+
}
|
315
|
+
|
316
|
+
items["transaction"] = name unless source_low_quality?
|
317
|
+
|
318
|
+
user = @hub.current_scope&.user
|
319
|
+
items["user_segment"] = user["segment"] if user && user["segment"]
|
320
|
+
|
321
|
+
items.compact!
|
322
|
+
@baggage = Baggage.new(items, mutable: false)
|
323
|
+
end
|
324
|
+
|
325
|
+
# These are high cardinality and thus bad
|
326
|
+
def source_low_quality?
|
327
|
+
source == :url
|
328
|
+
end
|
329
|
+
|
330
|
+
class SpanRecorder
|
331
|
+
attr_reader :max_length, :spans
|
332
|
+
|
333
|
+
def initialize(max_length)
|
334
|
+
@max_length = max_length
|
335
|
+
@spans = []
|
336
|
+
end
|
337
|
+
|
338
|
+
def add(span)
|
339
|
+
if @spans.count < @max_length
|
340
|
+
@spans << span
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# TransactionEvent represents events that carry transaction data (type: "transaction").
|
5
|
+
class TransactionEvent < Event
|
6
|
+
TYPE = "transaction"
|
7
|
+
|
8
|
+
# @return [<Array[Span]>]
|
9
|
+
attr_accessor :spans
|
10
|
+
|
11
|
+
# @return [Hash, nil]
|
12
|
+
attr_accessor :dynamic_sampling_context
|
13
|
+
|
14
|
+
# @return [Hash]
|
15
|
+
attr_accessor :measurements
|
16
|
+
|
17
|
+
# @return [Float, nil]
|
18
|
+
attr_reader :start_timestamp
|
19
|
+
|
20
|
+
def initialize(transaction:, **options)
|
21
|
+
super(**options)
|
22
|
+
|
23
|
+
self.transaction = transaction.name
|
24
|
+
self.transaction_info = { source: transaction.source }
|
25
|
+
self.contexts.merge!(transaction.contexts)
|
26
|
+
self.contexts.merge!(trace: transaction.get_trace_context)
|
27
|
+
self.timestamp = transaction.timestamp
|
28
|
+
self.start_timestamp = transaction.start_timestamp
|
29
|
+
self.tags = transaction.tags
|
30
|
+
self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context
|
31
|
+
self.measurements = transaction.measurements
|
32
|
+
|
33
|
+
finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
|
34
|
+
self.spans = finished_spans.map(&:to_hash)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Sets the event's start_timestamp.
|
38
|
+
# @param time [Time, Float]
|
39
|
+
# @return [void]
|
40
|
+
def start_timestamp=(time)
|
41
|
+
@start_timestamp = time.is_a?(Time) ? time.to_f : time
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Hash]
|
45
|
+
def to_hash
|
46
|
+
data = super
|
47
|
+
data[:spans] = @spans.map(&:to_hash) if @spans
|
48
|
+
data[:start_timestamp] = @start_timestamp
|
49
|
+
data[:measurements] = @measurements
|
50
|
+
data
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class Transport
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
|
7
|
+
attr_reader :transport_class
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@ssl_verification = true
|
11
|
+
@open_timeout = 1
|
12
|
+
@timeout = 2
|
13
|
+
@encoding = HTTPTransport::GZIP_ENCODING
|
14
|
+
end
|
15
|
+
|
16
|
+
def transport_class=(klass)
|
17
|
+
unless klass.is_a?(Class)
|
18
|
+
raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}")
|
19
|
+
end
|
20
|
+
|
21
|
+
@transport_class = klass
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class DummyTransport < Transport
|
5
|
+
attr_accessor :events, :envelopes
|
6
|
+
|
7
|
+
def initialize(*)
|
8
|
+
super
|
9
|
+
@events = []
|
10
|
+
@envelopes = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def send_event(event)
|
14
|
+
@events << event
|
15
|
+
end
|
16
|
+
|
17
|
+
def send_envelope(envelope)
|
18
|
+
@envelopes << envelope
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "zlib"
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
class HTTPTransport < Transport
|
8
|
+
GZIP_ENCODING = "gzip"
|
9
|
+
GZIP_THRESHOLD = 1024 * 30
|
10
|
+
CONTENT_TYPE = 'application/x-sentry-envelope'
|
11
|
+
|
12
|
+
DEFAULT_DELAY = 60
|
13
|
+
RETRY_AFTER_HEADER = "retry-after"
|
14
|
+
RATE_LIMIT_HEADER = "x-sentry-rate-limits"
|
15
|
+
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
|
16
|
+
|
17
|
+
def initialize(*args)
|
18
|
+
super
|
19
|
+
@endpoint = @dsn.envelope_endpoint
|
20
|
+
|
21
|
+
log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
|
22
|
+
end
|
23
|
+
|
24
|
+
def send_data(data)
|
25
|
+
encoding = ""
|
26
|
+
|
27
|
+
if should_compress?(data)
|
28
|
+
data = Zlib.gzip(data)
|
29
|
+
encoding = GZIP_ENCODING
|
30
|
+
end
|
31
|
+
|
32
|
+
headers = {
|
33
|
+
'Content-Type' => CONTENT_TYPE,
|
34
|
+
'Content-Encoding' => encoding,
|
35
|
+
'X-Sentry-Auth' => generate_auth_header,
|
36
|
+
'User-Agent' => USER_AGENT
|
37
|
+
}
|
38
|
+
|
39
|
+
response = conn.start do |http|
|
40
|
+
request = ::Net::HTTP::Post.new(@endpoint, headers)
|
41
|
+
request.body = data
|
42
|
+
http.request(request)
|
43
|
+
end
|
44
|
+
|
45
|
+
if response.code.match?(/\A2\d{2}/)
|
46
|
+
if has_rate_limited_header?(response)
|
47
|
+
handle_rate_limited_response(response)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
error_info = "the server responded with status #{response.code}"
|
51
|
+
|
52
|
+
if response.code == "429"
|
53
|
+
handle_rate_limited_response(response)
|
54
|
+
else
|
55
|
+
error_info += "\nbody: #{response.body}"
|
56
|
+
error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
|
57
|
+
end
|
58
|
+
|
59
|
+
raise Sentry::ExternalError, error_info
|
60
|
+
end
|
61
|
+
rescue SocketError => e
|
62
|
+
raise Sentry::ExternalError.new(e.message)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def has_rate_limited_header?(headers)
|
68
|
+
headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
|
69
|
+
end
|
70
|
+
|
71
|
+
def handle_rate_limited_response(headers)
|
72
|
+
rate_limits =
|
73
|
+
if rate_limits = headers[RATE_LIMIT_HEADER]
|
74
|
+
parse_rate_limit_header(rate_limits)
|
75
|
+
elsif retry_after = headers[RETRY_AFTER_HEADER]
|
76
|
+
# although Sentry doesn't send a date string back
|
77
|
+
# based on HTTP specification, this could be a date string (instead of an integer)
|
78
|
+
retry_after = retry_after.to_i
|
79
|
+
retry_after = DEFAULT_DELAY if retry_after == 0
|
80
|
+
|
81
|
+
{ nil => Time.now + retry_after }
|
82
|
+
else
|
83
|
+
{ nil => Time.now + DEFAULT_DELAY }
|
84
|
+
end
|
85
|
+
|
86
|
+
rate_limits.each do |category, limit|
|
87
|
+
if current_limit = @rate_limits[category]
|
88
|
+
if current_limit < limit
|
89
|
+
@rate_limits[category] = limit
|
90
|
+
end
|
91
|
+
else
|
92
|
+
@rate_limits[category] = limit
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def parse_rate_limit_header(rate_limit_header)
|
98
|
+
time = Time.now
|
99
|
+
|
100
|
+
result = {}
|
101
|
+
|
102
|
+
limits = rate_limit_header.split(",")
|
103
|
+
limits.each do |limit|
|
104
|
+
next if limit.nil? || limit.empty?
|
105
|
+
|
106
|
+
begin
|
107
|
+
retry_after, categories = limit.strip.split(":").first(2)
|
108
|
+
retry_after = time + retry_after.to_i
|
109
|
+
categories = categories.split(";")
|
110
|
+
|
111
|
+
if categories.empty?
|
112
|
+
result[nil] = retry_after
|
113
|
+
else
|
114
|
+
categories.each do |category|
|
115
|
+
result[category] = retry_after
|
116
|
+
end
|
117
|
+
end
|
118
|
+
rescue StandardError
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
def should_compress?(data)
|
126
|
+
@transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
|
127
|
+
end
|
128
|
+
|
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
|
+
|
151
|
+
def normalize_proxy(proxy)
|
152
|
+
return proxy unless proxy
|
153
|
+
|
154
|
+
case proxy
|
155
|
+
when String
|
156
|
+
uri = URI(proxy)
|
157
|
+
{ uri: uri, user: uri.user, password: uri.password }
|
158
|
+
when URI
|
159
|
+
{ uri: proxy, user: proxy.user, password: proxy.password }
|
160
|
+
when Hash
|
161
|
+
proxy
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def ssl_configuration
|
166
|
+
configuration = {
|
167
|
+
verify: @transport_configuration.ssl_verification,
|
168
|
+
ca_file: @transport_configuration.ssl_ca_file
|
169
|
+
}.merge(@transport_configuration.ssl || {})
|
170
|
+
|
171
|
+
configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
172
|
+
configuration
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|