sentry-ruby-core 6.3.1 → 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: 4e7e9d97ab7d4104210ca90edfc7bc1945beff554999c09312c1b8ab4b53472f
4
- data.tar.gz: d214dacdee8ac12cf8d6d18844ddc35fce588ed2ad2bd5299945ebc4f342ed00
3
+ metadata.gz: b945d3d2d16b0ba63298017ad761ef832b2b73a40071bfb1d7d660780827ba5b
4
+ data.tar.gz: 51132c054f3d252e41457b2386dccaf1552ccfed4d504164b70b44b1c6202f4b
5
5
  SHA512:
6
- metadata.gz: e35b0c356a78174237ba536629d6ebbf833a758eab82b9e6320c07d56b84c4f3b0b6a8a50a7627f7af4e045d7eb6dadb2a2bda487c4e8f11232152bd23720116
7
- data.tar.gz: 6ab13dae0fa67266128b9c5813b9e20dfa3e96d6852aae2179b0506a85b51947bb2650ec607784aaf9c20adb0469b2bce38c2ec14ef216f8dd85fa669809690d
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
@@ -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
 
@@ -87,6 +94,81 @@ module Sentry
87
94
  Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false)
88
95
  end
89
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
+
90
172
  def status_code_for_exception(exception)
91
173
  500
92
174
  end
data/lib/sentry/scope.rb CHANGED
@@ -98,7 +98,7 @@ module Sentry
98
98
  telemetry.attributes["sentry.release"] ||= configuration.release if configuration.release
99
99
  telemetry.attributes["server.address"] ||= configuration.server_name if configuration.server_name
100
100
 
101
- if configuration.send_default_pii && !user.empty?
101
+ unless user.empty?
102
102
  telemetry.attributes["user.id"] ||= user[:id] if user[:id]
103
103
  telemetry.attributes["user.name"] ||= user[:username] if user[:username]
104
104
  telemetry.attributes["user.email"] ||= user[:email] if user[:email]
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.1"
4
+ VERSION = "6.4.0"
5
5
  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.1
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.1
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.1
25
+ version: 6.4.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: concurrent-ruby
28
28
  requirement: !ruby/object:Gem::Requirement