sentry-ruby-core 6.3.0 → 6.4.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 +1 -1
- data/README.md +1 -1
- data/lib/sentry/baggage.rb +1 -1
- data/lib/sentry/configuration.rb +8 -0
- data/lib/sentry/dsn.rb +20 -0
- data/lib/sentry/metric_event.rb +1 -1
- data/lib/sentry/rack/capture_exceptions.rb +88 -2
- data/lib/sentry/scope.rb +20 -15
- data/lib/sentry/span.rb +3 -0
- data/lib/sentry/transport/http_transport.rb +1 -11
- data/lib/sentry/transport.rb +1 -1
- data/lib/sentry/version.rb +1 -1
- data/lib/sentry-ruby.rb +32 -0
- data/sentry-ruby.gemspec +1 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b945d3d2d16b0ba63298017ad761ef832b2b73a40071bfb1d7d660780827ba5b
|
|
4
|
+
data.tar.gz: 51132c054f3d252e41457b2386dccaf1552ccfed4d504164b70b44b1c6202f4b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c258db8a18282fcfb68093c09bccf32d9a0358819bc3a5ea5581fcec99e7aa147be5be47ad71055a4a0b16020ee51452d9e0b0cd43a99f0025bd767d546321ff
|
|
7
|
+
data.tar.gz: edcb0e412d362495836836d39db3753e7bc612968efe9bd036fefdaeb966d8f25541703c2b570c04679c223e421662cef20964d3958a23f471ee6e1794baf376
|
data/Gemfile
CHANGED
|
@@ -21,7 +21,7 @@ gem "puma"
|
|
|
21
21
|
|
|
22
22
|
gem "timecop"
|
|
23
23
|
gem "stackprof" unless RUBY_PLATFORM == "java"
|
|
24
|
-
gem "vernier", platforms: :ruby if
|
|
24
|
+
gem "vernier", platforms: :ruby if ruby_version >= Gem::Version.new("3.3") && ruby_version < Gem::Version.new("4.1.0")
|
|
25
25
|
|
|
26
26
|
gem "graphql", ">= 2.2.6"
|
|
27
27
|
|
data/README.md
CHANGED
|
@@ -33,7 +33,7 @@ If you're using `sentry-raven`, we recommend you to migrate to this new SDK. You
|
|
|
33
33
|
|
|
34
34
|
## Requirements
|
|
35
35
|
|
|
36
|
-
We test from Ruby 2.4 to Ruby
|
|
36
|
+
We test from Ruby 2.4 to Ruby 4.0 at the latest patchlevel/teeny version. We also support JRuby 9.0.
|
|
37
37
|
|
|
38
38
|
If you use self-hosted Sentry, please also make sure its version is above `20.6.0`.
|
|
39
39
|
|
data/lib/sentry/baggage.rb
CHANGED
data/lib/sentry/configuration.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "cgi/escape"
|
|
3
4
|
require "concurrent/utility/processor_counter"
|
|
4
5
|
|
|
5
6
|
require "sentry/utils/exception_cause_chain"
|
|
@@ -234,6 +235,12 @@ module Sentry
|
|
|
234
235
|
# @return [Boolean]
|
|
235
236
|
attr_accessor :send_default_pii
|
|
236
237
|
|
|
238
|
+
# Capture queue time from X-Request-Start header set by reverse proxies.
|
|
239
|
+
# Works with any Rack app behind Nginx, HAProxy, Heroku router, etc.
|
|
240
|
+
# Defaults to true.
|
|
241
|
+
# @return [Boolean]
|
|
242
|
+
attr_accessor :capture_queue_time
|
|
243
|
+
|
|
237
244
|
# Allow to skip Sentry emails within rake tasks
|
|
238
245
|
# @return [Boolean]
|
|
239
246
|
attr_accessor :skip_rake_integration
|
|
@@ -512,6 +519,7 @@ module Sentry
|
|
|
512
519
|
self.enable_backpressure_handling = false
|
|
513
520
|
self.trusted_proxies = []
|
|
514
521
|
self.dsn = ENV["SENTRY_DSN"]
|
|
522
|
+
self.capture_queue_time = true
|
|
515
523
|
|
|
516
524
|
spotlight_env = ENV["SENTRY_SPOTLIGHT"]
|
|
517
525
|
spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
|
data/lib/sentry/dsn.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "resolv"
|
|
|
6
6
|
|
|
7
7
|
module Sentry
|
|
8
8
|
class DSN
|
|
9
|
+
PROTOCOL_VERSION = "7"
|
|
9
10
|
PORT_MAP = { "http" => 80, "https" => 443 }.freeze
|
|
10
11
|
REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze
|
|
11
12
|
LOCALHOST_NAMES = %w[localhost 127.0.0.1 ::1 [::1]].freeze
|
|
@@ -54,6 +55,10 @@ module Sentry
|
|
|
54
55
|
"#{path}/api/#{project_id}/envelope/"
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
def otlp_traces_endpoint
|
|
59
|
+
"#{path}/api/#{project_id}/integration/otlp/v1/traces/"
|
|
60
|
+
end
|
|
61
|
+
|
|
57
62
|
def local?
|
|
58
63
|
@local ||= (localhost? || private_ip? || resolved_ips_private?)
|
|
59
64
|
end
|
|
@@ -81,5 +86,20 @@ module Sentry
|
|
|
81
86
|
end
|
|
82
87
|
end
|
|
83
88
|
end
|
|
89
|
+
|
|
90
|
+
def generate_auth_header(client: nil)
|
|
91
|
+
now = Sentry.utc_now.to_i
|
|
92
|
+
|
|
93
|
+
fields = {
|
|
94
|
+
"sentry_version" => PROTOCOL_VERSION,
|
|
95
|
+
"sentry_timestamp" => now,
|
|
96
|
+
"sentry_key" => @public_key
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fields["sentry_client"] = client if client
|
|
100
|
+
fields["sentry_secret"] = @secret_key if @secret_key
|
|
101
|
+
|
|
102
|
+
"Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
|
|
103
|
+
end
|
|
84
104
|
end
|
|
85
105
|
end
|
data/lib/sentry/metric_event.rb
CHANGED
|
@@ -33,7 +33,7 @@ module Sentry
|
|
|
33
33
|
raise # Don't capture Sentry errors
|
|
34
34
|
rescue Exception => e
|
|
35
35
|
capture_exception(e, env)
|
|
36
|
-
finish_transaction(transaction,
|
|
36
|
+
finish_transaction(transaction, status_code_for_exception(e))
|
|
37
37
|
raise
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -72,7 +72,14 @@ module Sentry
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
transaction = Sentry.continue_trace(env, **options)
|
|
75
|
-
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
|
75
|
+
transaction = Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
|
76
|
+
|
|
77
|
+
# attach queue time if available
|
|
78
|
+
if transaction && (queue_time = extract_queue_time(env))
|
|
79
|
+
transaction.set_data(Span::DataConventions::HTTP_QUEUE_TIME_MS, queue_time)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
transaction
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
|
|
@@ -86,6 +93,85 @@ module Sentry
|
|
|
86
93
|
def mechanism
|
|
87
94
|
Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
|
|
88
95
|
end
|
|
96
|
+
|
|
97
|
+
# Extracts queue time from the request environment.
|
|
98
|
+
# Calculates the time (in milliseconds) the request spent waiting in the
|
|
99
|
+
# web server queue before processing began.
|
|
100
|
+
#
|
|
101
|
+
# Subtracts puma.request_body_wait to account for time spent waiting for
|
|
102
|
+
# slow clients to send the request body, isolating actual queue time.
|
|
103
|
+
# See: https://github.com/puma/puma/blob/master/docs/architecture.md
|
|
104
|
+
#
|
|
105
|
+
# @param env [Hash] Rack env
|
|
106
|
+
# @return [Float, nil] queue time in milliseconds or nil
|
|
107
|
+
def extract_queue_time(env)
|
|
108
|
+
return unless Sentry.configuration&.capture_queue_time
|
|
109
|
+
|
|
110
|
+
header_value = env["HTTP_X_REQUEST_START"]
|
|
111
|
+
return unless header_value
|
|
112
|
+
|
|
113
|
+
request_start = parse_request_start_header(header_value)
|
|
114
|
+
return unless request_start
|
|
115
|
+
|
|
116
|
+
total_time_ms = ((Time.now.to_f - request_start) * 1000).round(2)
|
|
117
|
+
|
|
118
|
+
# reject negative (clock skew between proxy & app server)
|
|
119
|
+
return unless total_time_ms >= 0
|
|
120
|
+
|
|
121
|
+
puma_wait_ms = env["puma.request_body_wait"]
|
|
122
|
+
puma_wait_ms = puma_wait_ms.to_f if puma_wait_ms.is_a?(String)
|
|
123
|
+
|
|
124
|
+
if puma_wait_ms && puma_wait_ms > 0
|
|
125
|
+
queue_time_ms = total_time_ms - puma_wait_ms
|
|
126
|
+
queue_time_ms >= 0 ? queue_time_ms : 0.0 # more sanity check
|
|
127
|
+
else
|
|
128
|
+
total_time_ms
|
|
129
|
+
end
|
|
130
|
+
rescue StandardError
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Parses X-Request-Start header value to extract a timestamp.
|
|
134
|
+
# Supports multiple formats:
|
|
135
|
+
# - Nginx: "t=1234567890.123" (seconds with decimal)
|
|
136
|
+
# - Heroku, HAProxy 1.9+: "t=1234567890123456" (microseconds)
|
|
137
|
+
# - HAProxy < 1.9: "t=1234567890" (seconds)
|
|
138
|
+
# - Generic: "1234567890.123" (raw timestamp)
|
|
139
|
+
#
|
|
140
|
+
# @param header_value [String] The X-Request-Start header value
|
|
141
|
+
# @return [Float, nil] Timestamp in seconds since epoch or nil
|
|
142
|
+
def parse_request_start_header(header_value)
|
|
143
|
+
return unless header_value
|
|
144
|
+
|
|
145
|
+
# Take the first value if comma-separated (multiple headers collapsed by a proxy)
|
|
146
|
+
# and strip surrounding whitespace from each token
|
|
147
|
+
raw = header_value.split(",").first.to_s.strip
|
|
148
|
+
|
|
149
|
+
timestamp = if raw.start_with?("t=")
|
|
150
|
+
value = raw[2..-1].strip
|
|
151
|
+
return nil unless value.match?(/\A\d+(?:\.\d+)?\z/)
|
|
152
|
+
value.to_f
|
|
153
|
+
elsif raw.match?(/\A\d+(?:\.\d+)?\z/)
|
|
154
|
+
raw.to_f
|
|
155
|
+
else
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# normalize: timestamps can be in seconds, milliseconds or microseconds
|
|
160
|
+
# any timestamp > 10 trillion = microseconds
|
|
161
|
+
if timestamp > 10_000_000_000_000
|
|
162
|
+
timestamp / 1_000_000.0
|
|
163
|
+
# timestamp > 10 billion & < 10 trillion = milliseconds
|
|
164
|
+
elsif timestamp > 10_000_000_000
|
|
165
|
+
timestamp / 1_000.0
|
|
166
|
+
else
|
|
167
|
+
timestamp # assume seconds
|
|
168
|
+
end
|
|
169
|
+
rescue StandardError
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def status_code_for_exception(exception)
|
|
173
|
+
500
|
|
174
|
+
end
|
|
89
175
|
end
|
|
90
176
|
end
|
|
91
177
|
end
|
data/lib/sentry/scope.rb
CHANGED
|
@@ -60,19 +60,10 @@ module Sentry
|
|
|
60
60
|
event.attachments = attachments
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
event.dynamic_sampling_context ||= span.get_dynamic_sampling_context
|
|
68
|
-
end
|
|
69
|
-
else
|
|
70
|
-
event.contexts[:trace] ||= propagation_context.get_trace_context
|
|
71
|
-
|
|
72
|
-
if event.respond_to?(:dynamic_sampling_context)
|
|
73
|
-
event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context
|
|
74
|
-
end
|
|
75
|
-
end
|
|
63
|
+
trace_context = get_trace_context
|
|
64
|
+
dynamic_sampling_context = trace_context.delete(:dynamic_sampling_context)
|
|
65
|
+
event.contexts[:trace] ||= trace_context
|
|
66
|
+
event.dynamic_sampling_context ||= dynamic_sampling_context
|
|
76
67
|
|
|
77
68
|
all_event_processors = self.class.global_event_processors + @event_processors
|
|
78
69
|
|
|
@@ -94,7 +85,7 @@ module Sentry
|
|
|
94
85
|
# @return [MetricEvent, LogEvent] the telemetry event with scope context applied
|
|
95
86
|
def apply_to_telemetry(telemetry)
|
|
96
87
|
# TODO-neel when new scope set_attribute api is added: add them here
|
|
97
|
-
trace_context =
|
|
88
|
+
trace_context = get_trace_context
|
|
98
89
|
telemetry.trace_id = trace_context[:trace_id]
|
|
99
90
|
telemetry.span_id = trace_context[:span_id]
|
|
100
91
|
|
|
@@ -107,7 +98,7 @@ module Sentry
|
|
|
107
98
|
telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
|
|
108
99
|
telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
|
|
109
100
|
|
|
110
|
-
|
|
101
|
+
unless user.empty?
|
|
111
102
|
telemetry.attributes["user.id"] ||= user[:id] if user[:id]
|
|
112
103
|
telemetry.attributes["user.name"] ||= user[:username] if user[:username]
|
|
113
104
|
telemetry.attributes["user.email"] ||= user[:email] if user[:email]
|
|
@@ -305,6 +296,20 @@ module Sentry
|
|
|
305
296
|
span
|
|
306
297
|
end
|
|
307
298
|
|
|
299
|
+
# Returns the trace context for this scope.
|
|
300
|
+
# Prioritizes external propagation context (from OTel) over local propagation context.
|
|
301
|
+
# @return [Hash]
|
|
302
|
+
def get_trace_context
|
|
303
|
+
if span
|
|
304
|
+
span.get_trace_context.merge(dynamic_sampling_context: span.get_dynamic_sampling_context)
|
|
305
|
+
elsif (external_context = Sentry.get_external_propagation_context)
|
|
306
|
+
trace_id, span_id = external_context
|
|
307
|
+
{ trace_id: trace_id, span_id: span_id }
|
|
308
|
+
else
|
|
309
|
+
propagation_context.get_trace_context.merge(dynamic_sampling_context: propagation_context.get_dynamic_sampling_context)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
308
313
|
# Sets the scope's fingerprint attribute.
|
|
309
314
|
# @param fingerprint [Array]
|
|
310
315
|
# @return [Array]
|
data/lib/sentry/span.rb
CHANGED
|
@@ -49,6 +49,9 @@ module Sentry
|
|
|
49
49
|
MESSAGING_DESTINATION_NAME = "messaging.destination.name"
|
|
50
50
|
MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency"
|
|
51
51
|
MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count"
|
|
52
|
+
|
|
53
|
+
# Time in ms the request spent in the server queue before processing began.
|
|
54
|
+
HTTP_QUEUE_TIME_MS = "http.server.request.time_in_queue"
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
STATUS_MAP = {
|
|
@@ -69,17 +69,7 @@ module Sentry
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def generate_auth_header
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
now = Sentry.utc_now.to_i
|
|
75
|
-
fields = {
|
|
76
|
-
"sentry_version" => PROTOCOL_VERSION,
|
|
77
|
-
"sentry_client" => USER_AGENT,
|
|
78
|
-
"sentry_timestamp" => now,
|
|
79
|
-
"sentry_key" => @dsn.public_key
|
|
80
|
-
}
|
|
81
|
-
fields["sentry_secret"] = @dsn.secret_key if @dsn.secret_key
|
|
82
|
-
"Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
|
|
72
|
+
@dsn&.generate_auth_header(client: USER_AGENT)
|
|
83
73
|
end
|
|
84
74
|
|
|
85
75
|
def conn
|
data/lib/sentry/transport.rb
CHANGED
data/lib/sentry/version.rb
CHANGED
data/lib/sentry-ruby.rb
CHANGED
|
@@ -666,6 +666,38 @@ module Sentry
|
|
|
666
666
|
META
|
|
667
667
|
end
|
|
668
668
|
|
|
669
|
+
# Registers a callback function that retrieves the current external propagation context.
|
|
670
|
+
# This is used by OpenTelemetry integration to provide trace_id and span_id from OTel context.
|
|
671
|
+
#
|
|
672
|
+
# @param callback [Proc, nil] A callable that returns [trace_id, span_id] or nil
|
|
673
|
+
# @return [void]
|
|
674
|
+
#
|
|
675
|
+
# @example
|
|
676
|
+
# Sentry.register_external_propagation_context do
|
|
677
|
+
# span_context = OpenTelemetry::Trace.current_span.context
|
|
678
|
+
# return nil unless span_context.valid?
|
|
679
|
+
# [span_context.hex_trace_id, span_context.hex_span_id]
|
|
680
|
+
# end
|
|
681
|
+
def register_external_propagation_context(&callback)
|
|
682
|
+
@external_propagation_context_callback = callback
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
# Returns the external propagation context (trace_id, span_id) if a callback is registered.
|
|
686
|
+
#
|
|
687
|
+
# @return [Array<String>, nil] A tuple of [trace_id, span_id] or nil if no context is available
|
|
688
|
+
def get_external_propagation_context
|
|
689
|
+
return nil unless @external_propagation_context_callback
|
|
690
|
+
|
|
691
|
+
@external_propagation_context_callback.call
|
|
692
|
+
rescue => e
|
|
693
|
+
sdk_logger&.debug(LOGGER_PROGNAME) { "Error getting external propagation context: #{e.message}" } if initialized?
|
|
694
|
+
nil
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def clear_external_propagation_context
|
|
698
|
+
@external_propagation_context_callback = nil
|
|
699
|
+
end
|
|
700
|
+
|
|
669
701
|
# @!visibility private
|
|
670
702
|
def utc_now
|
|
671
703
|
Time.now.utc
|
data/sentry-ruby.gemspec
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sentry-ruby-core
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sentry Team
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 6.
|
|
18
|
+
version: 6.4.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 6.
|
|
25
|
+
version: 6.4.0
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: concurrent-ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|