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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dcf2768081f524baa62e2df5f33a2b1bc927f6ed818e17481376e63e83153b2
4
- data.tar.gz: 71c6db91301a24a1b05afa798a249de12b31a5d68c45d16f138f73c0999b3183
3
+ metadata.gz: b945d3d2d16b0ba63298017ad761ef832b2b73a40071bfb1d7d660780827ba5b
4
+ data.tar.gz: 51132c054f3d252e41457b2386dccaf1552ccfed4d504164b70b44b1c6202f4b
5
5
  SHA512:
6
- metadata.gz: 32e0f2534e8785d3e145f574ef8cbc66abdd6300ed4b0695d69a5a8f4788870d1d0c89da5257ce5de9114505b90b0902981afae0365e028d7e4e8c8caec00d23
7
- data.tar.gz: 5072c6873787d7e070050df7aeeb3e9040ca3add4c04f1c4e21c63d12fb241ee6615eced7f71fac5edeee4381d60272069bdc38ce967033ead0e2e76894b2573
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 RUBY_VERSION >= "3.2.1"
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 3.4 at the latest patchlevel/teeny version. We also support JRuby 9.0.
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
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "cgi"
3
+ require "cgi/escape"
4
4
 
5
5
  module Sentry
6
6
  # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation.
@@ -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
@@ -33,7 +33,7 @@ module Sentry
33
33
  type: @type,
34
34
  value: @value,
35
35
  unit: @unit,
36
- timestamp: @timestamp,
36
+ timestamp: @timestamp.to_f,
37
37
  trace_id: @trace_id,
38
38
  span_id: @span_id,
39
39
  attributes: serialize_attributes
@@ -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, 500)
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
- if span
64
- event.contexts[:trace] ||= span.get_trace_context
65
-
66
- if event.respond_to?(:dynamic_sampling_context)
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 = span ? span.get_trace_context : propagation_context.get_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
- if configuration.send_default_pii && !user.empty?
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
- return nil unless @dsn
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
@@ -5,7 +5,7 @@ require "sentry/envelope"
5
5
 
6
6
  module Sentry
7
7
  class Transport
8
- PROTOCOL_VERSION = "7"
8
+ PROTOCOL_VERSION = DSN::PROTOCOL_VERSION
9
9
  USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
10
10
  CLIENT_REPORT_INTERVAL = 30
11
11
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "6.3.0"
4
+ VERSION = "6.4.0"
5
5
  end
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
@@ -30,4 +30,5 @@ Gem::Specification.new do |spec|
30
30
 
31
31
  spec.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
32
32
  spec.add_dependency "bigdecimal"
33
+ spec.add_dependency "logger"
33
34
  end
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.3.0
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.3.0
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.3.0
25
+ version: 6.4.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: concurrent-ruby
28
28
  requirement: !ruby/object:Gem::Requirement