From 39a875befee02184a49b5bb9d6b8d76352d72937 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 09:41:00 +0100 Subject: [PATCH 01/12] add configuration for capturing queue time default to true --- sentry-ruby/lib/sentry/configuration.rb | 7 +++++++ sentry-ruby/spec/sentry/configuration_spec.rb | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/sentry-ruby/lib/sentry/configuration.rb b/sentry-ruby/lib/sentry/configuration.rb index b09f8ad62..8b27699cb 100644 --- a/sentry-ruby/lib/sentry/configuration.rb +++ b/sentry-ruby/lib/sentry/configuration.rb @@ -234,6 +234,12 @@ class Configuration # @return [Boolean] attr_accessor :send_default_pii + # Capture queue time from X-Request-Start header set by reverse proxies. + # Works with any Rack app behind Nginx, HAProxy, Heroku router, etc. + # Defaults to true. + # @return [Boolean] + attr_accessor :capture_queue_time + # Allow to skip Sentry emails within rake tasks # @return [Boolean] attr_accessor :skip_rake_integration @@ -512,6 +518,7 @@ def initialize self.enable_backpressure_handling = false self.trusted_proxies = [] self.dsn = ENV["SENTRY_DSN"] + self.capture_queue_time = true spotlight_env = ENV["SENTRY_SPOTLIGHT"] spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true) diff --git a/sentry-ruby/spec/sentry/configuration_spec.rb b/sentry-ruby/spec/sentry/configuration_spec.rb index c84de8e99..e8aab303a 100644 --- a/sentry-ruby/spec/sentry/configuration_spec.rb +++ b/sentry-ruby/spec/sentry/configuration_spec.rb @@ -329,6 +329,17 @@ expect { subject.before_breadcrumb = true }.to raise_error(ArgumentError, "before_breadcrumb must be callable (or nil to disable)") end + describe "#capture_queue_time" do + it "defaults to true" do + expect(subject.capture_queue_time).to eq(true) + end + + it "can be set to false" do + subject.capture_queue_time = false + expect(subject.capture_queue_time).to eq(false) + end + end + context 'being initialized with a current environment' do before(:each) do subject.environment = 'test' From 8b2e2b8ec8193828ca3082745ca4727315b9b6f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 10:12:07 +0100 Subject: [PATCH 02/12] Extract, normalize & add queue time to transaction --- .../lib/sentry/rack/capture_exceptions.rb | 81 +++++++++++++++++- sentry-ruby/lib/sentry/span.rb | 3 + .../sentry/rack/capture_exceptions_spec.rb | 85 +++++++++++++++++++ 3 files changed, 168 insertions(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 40cdfb4f8..7bcadc4fb 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -72,7 +72,14 @@ def start_transaction(env, scope) } transaction = Sentry.continue_trace(env, **options) - Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) + transaction = Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) + + # attach queue time if available + if transaction && (queue_time = extract_queue_time(env)) + transaction.set_data(Span::DataConventions::HTTP_QUEUE_TIME_MS, queue_time) + end + + transaction end @@ -86,6 +93,78 @@ def finish_transaction(transaction, status_code) def mechanism Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false) end + + # Extracts queue time from the request environment. + # Calculates the time (in milliseconds) the request spent waiting in the + # web server queue before processing began. + # + # Subtracts puma.request_body_wait to account for time spent waiting for + # slow clients to send the request body, isolating actual queue time. + # See: https://github.com/puma/puma/blob/master/docs/architecture.md + # + # @param env [Hash] Rack env + # @return [Float, nil] queue time in milliseconds or nil + def extract_queue_time(env) + return nil unless Sentry.configuration.capture_queue_time + + header_value = env["HTTP_X_REQUEST_START"] + return nil unless header_value + + request_start = parse_request_start_header(header_value) + return nil unless request_start + + total_time_ms = ((Time.now.to_f - request_start) * 1000).round(2) + + # reject negative (clock skew between proxy & app server) or very large values (> 60 seconds) + return nil unless total_time_ms >= 0 && total_time_ms < 60_000 + + puma_wait_ms = env["puma.request_body_wait"] + + if puma_wait_ms && puma_wait_ms > 0 + queue_time_ms = total_time_ms - puma_wait_ms + queue_time_ms >= 0 ? queue_time_ms : 0.0 # more sanity check + else + total_time_ms + end + rescue StandardError + nil + end + + # Parses X-Request-Start header value to extract a timestamp. + # Supports multiple formats: + # - Nginx: "t=1234567890.123" (seconds with decimal) + # - Heroku, HAProxy 1.9+: "t=1234567890123456" (microseconds) + # - HAProxy < 1.9: "t=1234567890" (seconds) + # - Generic: "1234567890.123" (raw timestamp) + # + # @param header_value [String] The X-Request-Start header value + # @return [Float, nil] Timestamp in seconds since epoch or nil + def parse_request_start_header(header_value) + return nil unless header_value + + # handle format: t= + timestamp = if header_value =~ /t=(\d+\.?\d*)/ + $1.to_f + # handle raw timestamp format + elsif header_value =~ /^(\d+\.?\d*)$/ + $1.to_f + else + return nil + end + + # normalize: timestamps can be in seconds, milliseconds or microseconds + # any timestamp > 10 trillion = microseconds + if timestamp > 10_000_000_000_000 + timestamp / 1_000_000.0 + # timestamp > 10 billion & < 10 trillion = milliseconds + elsif timestamp > 10_000_000_000 + timestamp / 1_000.0 + else + timestamp # assume seconds + end + rescue StandardError + nil + end end end end diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 5ba4b7cf8..7510c88dc 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -49,6 +49,9 @@ module DataConventions MESSAGING_DESTINATION_NAME = "messaging.destination.name" MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" + + # Time in ms the request spent in the server queue before processing began. + HTTP_QUEUE_TIME_MS = "http.queue_time_ms" end STATUS_MAP = { diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index 976d87984..6f99ecdb2 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -660,6 +660,91 @@ def will_be_sampled_by_sdk end end + describe "queue time capture" do + let(:stack) do + app = ->(_) { [200, {}, ['ok']] } + Sentry::Rack::CaptureExceptions.new(app) + end + + before do + perform_basic_setup do |config| + config.traces_sample_rate = 1.0 + end + end + + let(:transaction) { last_sentry_event } + + context "with X-Request-Start header" do + it "attaches queue time to transaction" do + timestamp = Time.now.to_f - 0.05 # 50ms ago + env["HTTP_X_REQUEST_START"] = "t=#{timestamp}" + + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_within(10).of(50) + end + + it "subtracts puma.request_body_wait" do + timestamp = Time.now.to_f - 0.1 # 100ms ago + env["HTTP_X_REQUEST_START"] = "t=#{timestamp}" + env["puma.request_body_wait"] = 40 # 40ms waiting for client + + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_within(10).of(60) # 100 - 40 + end + + it "handles different timestamp formats" do + # Heroku/HAProxy microseconds format + timestamp_us = ((Time.now.to_f - 0.03) * 1_000_000).to_i + env["HTTP_X_REQUEST_START"] = "t=#{timestamp_us}" + + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_within(10).of(30) + end + end + + context "without X-Request-Start header" do + it "doesn't add queue time data" do + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_nil + end + end + + context "with invalid header" do + it "doesn't add queue time data" do + env["HTTP_X_REQUEST_START"] = "invalid" + + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_nil + end + end + + context "when capture_queue_time is disabled" do + before do + Sentry.configuration.capture_queue_time = false + end + + it "doesn't capture queue time" do + timestamp = Time.now.to_f - 0.05 + env["HTTP_X_REQUEST_START"] = "t=#{timestamp}" + + stack.call(env) + + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_nil + end + end + end + describe "tracing without performance" do let(:incoming_prop_context) { Sentry::PropagationContext.new(Sentry::Scope.new) } let(:env) do From 7829e24249e579b8195878740541eb9965d87103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 11:13:24 +0100 Subject: [PATCH 03/12] Test app for queue time in Sentry --- test_app/sentry_queue_test.rb | 234 ++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 test_app/sentry_queue_test.rb diff --git a/test_app/sentry_queue_test.rb b/test_app/sentry_queue_test.rb new file mode 100644 index 000000000..c58ac7c67 --- /dev/null +++ b/test_app/sentry_queue_test.rb @@ -0,0 +1,234 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Sentry Queue Time Test Generator +# +# USAGE: +# export SENTRY_DSN='https://your-key@o123.ingest.us.sentry.io/456' +# export SENTRY_SKIP_SSL_VERIFY=true # if you get SSL errors +# ruby sentry_queue_test.rb [duration_minutes] [requests_per_minute] [pattern] +# +# EXAMPLES: +# ruby sentry_queue_test.rb # 5 min, realistic pattern +# ruby sentry_queue_test.rb 10 30 spike # 10 min, spike pattern +# ruby sentry_queue_test.rb 5 20 steady # 5 min, steady pattern +# +# PATTERNS: +# realistic - Business hours pattern (peak 9am-5pm) +# spike - Sudden traffic spikes +# degradation - Gradual performance decline +# recovery - System recovering after incident +# steady - Consistent baseline +# wave - Smooth sine wave + +require 'bundler/inline' + +gemfile do + source 'https://rubygems.org' + gem 'rack' + gem 'sentry-ruby', path: File.expand_path('../../sentry-ruby', __dir__) +end + +require 'rack' +require 'sentry-ruby' + +# Configuration +DURATION_MINUTES = (ARGV[0] || 5).to_i +REQUESTS_PER_MINUTE = (ARGV[1] || 20).to_i +PATTERN = (ARGV[2] || 'realistic').downcase + +# Transaction sequences - realistic API patterns +TRANSACTION_SEQUENCES = { + user_journey: [ + { path: '/api/products', weight: 0.4 }, + { path: '/api/products/:id', weight: 0.25 }, + { path: '/api/cart', weight: 0.2 }, + { path: '/api/orders', weight: 0.1 }, + { path: '/api/payment', weight: 0.05 } + ], + + admin: [ + { path: '/api/admin/auth', weight: 0.1 }, + { path: '/api/admin/dashboard', weight: 0.3 }, + { path: '/api/admin/users', weight: 0.25 }, + { path: '/api/admin/reports', weight: 0.2 }, + { path: '/api/admin/analytics', weight: 0.15 } + ], + + background: [ + { path: '/api/webhooks/stripe', weight: 0.3 }, + { path: '/api/jobs/email', weight: 0.25 }, + { path: '/api/jobs/export', weight: 0.2 }, + { path: '/api/jobs/cleanup', weight: 0.15 }, + { path: '/api/cron/daily', weight: 0.1 } + ] +} + +# Queue time patterns +PATTERNS = { + 'realistic' => lambda do |progress| + hour_of_day = (progress * 24) % 24 + if hour_of_day >= 9 && hour_of_day <= 17 + base = 40 + (Math.sin((hour_of_day - 9) / 8.0 * Math::PI) * 30) + else + base = 10 + rand * 10 + end + base + (rand * 20 - 10) + end, + + 'spike' => lambda do |progress| + spike_phase = (progress * 5) % 1 + spike_phase < 0.15 ? 150 + rand * 100 : 15 + rand * 20 + end, + + 'degradation' => lambda do |progress| + base = progress * 200 + base + (rand * 50 - 25) + end, + + 'recovery' => lambda do |progress| + base = (1 - progress) * 200 + 10 + base + (rand * 30 - 15) + end, + + 'steady' => lambda do |progress| + 30 + rand * 20 + end, + + 'wave' => lambda do |progress| + Math.sin(progress * Math::PI * 2) * 50 + 60 + (rand * 20 - 10) + end +} + +unless PATTERNS.key?(PATTERN) + puts "\nError: Unknown pattern '#{PATTERN}'" + puts "\nAvailable patterns: #{PATTERNS.keys.join(', ')}" + exit 1 +end + +# Sentry setup +unless ENV['SENTRY_DSN'] + puts "\nError: SENTRY_DSN environment variable not set" + puts "\nSet it with:" + puts " export SENTRY_DSN='https://your-key@o123.ingest.us.sentry.io/456'" + exit 1 +end + +skip_ssl = ENV['SENTRY_SKIP_SSL_VERIFY'] == 'true' + +Sentry.init do |config| + config.dsn = ENV['SENTRY_DSN'] + config.traces_sample_rate = 1.0 + config.capture_queue_time = true + config.transport.ssl_verification = !skip_ssl +end + +# App setup +app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } +middleware = Sentry::Rack::CaptureExceptions.new(app) + +# Helper functions +def weighted_random_endpoint(sequences) + all_endpoints = sequences.flat_map { |_name, endpoints| endpoints } + total_weight = all_endpoints.sum { |ep| ep[:weight] } + random = rand * total_weight + + cumulative = 0 + all_endpoints.each do |endpoint| + cumulative += endpoint[:weight] + return endpoint[:path] if random <= cumulative + end + + all_endpoints.last[:path] +end + +def progress_bar(current, total, width = 40) + percent = (current / total.to_f * 100).round(1) + filled = (current / total.to_f * width).round + bar = "█" * filled + "░" * (width - filled) + "[#{bar}] #{percent}%" +end + +def format_time(seconds) + seconds < 60 ? "#{seconds.round}s" : "#{(seconds / 60).round(1)}min" +end + +# Main execution +puts "\nSentry Queue Time Test" +puts "=" * 70 +puts "Pattern: #{PATTERN}" +puts "Duration: #{DURATION_MINUTES} minutes" +puts "Frequency: #{REQUESTS_PER_MINUTE} req/min" +puts "Total: #{DURATION_MINUTES * REQUESTS_PER_MINUTE} requests" +puts "\nStarting in 2 seconds... (Ctrl+C to cancel)" +sleep 2 + +interval_seconds = 60.0 / REQUESTS_PER_MINUTE +start_time = Time.now +end_time = start_time + (DURATION_MINUTES * 60) +request_num = 0 + +puts "\nGenerating transactions... (Ctrl+C to stop)\n" + +begin + while Time.now < end_time + request_num += 1 + elapsed_seconds = Time.now - start_time + elapsed_minutes = elapsed_seconds / 60.0 + progress = elapsed_minutes / DURATION_MINUTES.to_f + + # Generate queue time + queue_time_ms = PATTERNS[PATTERN].call(progress) + queue_time_ms = [[queue_time_ms, 1].max, 1000].min + + # Select endpoint + endpoint = weighted_random_endpoint(TRANSACTION_SEQUENCES) + + # Create request + request_start_time = Time.now.to_f - (queue_time_ms / 1000.0) + + env = Rack::MockRequest.env_for(endpoint) + env['HTTP_X_REQUEST_START'] = "t=#{request_start_time}" + env['REQUEST_METHOD'] = ['GET', 'POST', 'PUT', 'DELETE'].sample + + # Add Puma wait occasionally + if rand < 0.15 + puma_wait = (rand * 40).round + env['puma.request_body_wait'] = puma_wait + actual_queue = [queue_time_ms - puma_wait, 0].max + else + actual_queue = queue_time_ms + end + + # Send transaction + middleware.call(env) + + # Progress display + total_requests = DURATION_MINUTES * REQUESTS_PER_MINUTE + remaining_seconds = end_time - Time.now + + print "\r#{progress_bar(request_num, total_requests)} " + print "#{request_num}/#{total_requests} | " + print "Queue: #{actual_queue.round(1)}ms | " + print "Remaining: #{format_time(remaining_seconds)} " + $stdout.flush + + sleep interval_seconds + end + + print "\r" + " " * 100 + "\r" + +rescue Interrupt + print "\r" + " " * 100 + "\r" + puts "\nStopped by user" +end + +# Summary +duration = ((Time.now - start_time) / 60.0).round(2) + +puts "\n" + "=" * 70 +puts "Complete" +puts "=" * 70 +puts "\nDuration: #{duration} minutes" +puts "Requests: #{request_num}" +puts "Pattern: #{PATTERN}" \ No newline at end of file From dfea02f0afb15ff0253570165533a5ebcdf4a8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 11:26:47 +0100 Subject: [PATCH 04/12] Fix redos issue --- sentry-ruby/lib/sentry/rack/capture_exceptions.rb | 10 ++++------ test_app/sentry_queue_test.rb | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index 7bcadc4fb..efd312e68 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -142,12 +142,10 @@ def extract_queue_time(env) def parse_request_start_header(header_value) return nil unless header_value - # handle format: t= - timestamp = if header_value =~ /t=(\d+\.?\d*)/ - $1.to_f - # handle raw timestamp format - elsif header_value =~ /^(\d+\.?\d*)$/ - $1.to_f + timestamp = if header_value.start_with?("t=") + header_value[2..-1].to_f + elsif header_value.match?(/\A\d+(?:\.\d+)?\z/) + header_value.to_f else return nil end diff --git a/test_app/sentry_queue_test.rb b/test_app/sentry_queue_test.rb index c58ac7c67..e76e355a9 100644 --- a/test_app/sentry_queue_test.rb +++ b/test_app/sentry_queue_test.rb @@ -231,4 +231,4 @@ def format_time(seconds) puts "=" * 70 puts "\nDuration: #{duration} minutes" puts "Requests: #{request_num}" -puts "Pattern: #{PATTERN}" \ No newline at end of file +puts "Pattern: #{PATTERN}" From c223fcefc6a5e754907a4d53c9a6c096c28d8d67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 11:29:15 +0100 Subject: [PATCH 05/12] move test app --- {test_app => sentry-ruby/test-app}/sentry_queue_test.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {test_app => sentry-ruby/test-app}/sentry_queue_test.rb (100%) diff --git a/test_app/sentry_queue_test.rb b/sentry-ruby/test-app/sentry_queue_test.rb similarity index 100% rename from test_app/sentry_queue_test.rb rename to sentry-ruby/test-app/sentry_queue_test.rb From 597c2e62c86d63d121b764ce7dbb1385c3dedc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 11:34:06 +0100 Subject: [PATCH 06/12] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62448291a..6dc0bfa81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- Queue time capture for Rack ([#2838](https://github.com/getsentry/sentry-ruby/pull/2838)) + - Implement new `Sentry.metrics` functionality ([#2818](https://github.com/getsentry/sentry-ruby/pull/2818)) The SDK now supports Sentry's new [Trace Connected Metrics](https://docs.sentry.io/product/explore/metrics/) product. From 9f3bd01ec4ead272d33aa957f471781486c186c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 11:43:59 +0100 Subject: [PATCH 07/12] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc0bfa81..b43500109 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ +## Unreleased + +- Queue time capture for Rack ([#2838](https://github.com/getsentry/sentry-ruby/pull/2838)) + ## 6.3.0 ### Features -- Queue time capture for Rack ([#2838](https://github.com/getsentry/sentry-ruby/pull/2838)) - - Implement new `Sentry.metrics` functionality ([#2818](https://github.com/getsentry/sentry-ruby/pull/2818)) The SDK now supports Sentry's new [Trace Connected Metrics](https://docs.sentry.io/product/explore/metrics/) product. From 1407b7d62c5a3638639fb03b996f758bdd1f0265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= <114897+dingsdax@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:25:18 +0100 Subject: [PATCH 08/12] Update sentry-ruby/lib/sentry/rack/capture_exceptions.rb Co-authored-by: Neel Shah --- sentry-ruby/lib/sentry/rack/capture_exceptions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index efd312e68..d729f89d8 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -105,7 +105,7 @@ def mechanism # @param env [Hash] Rack env # @return [Float, nil] queue time in milliseconds or nil def extract_queue_time(env) - return nil unless Sentry.configuration.capture_queue_time + return nil unless Sentry.configuration&.capture_queue_time header_value = env["HTTP_X_REQUEST_START"] return nil unless header_value From f5e132b627c02a5b0067e7ec128a8cb835350681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 15:01:29 +0100 Subject: [PATCH 09/12] Delete sentry_queue_test.rb --- sentry-ruby/test-app/sentry_queue_test.rb | 234 ---------------------- 1 file changed, 234 deletions(-) delete mode 100644 sentry-ruby/test-app/sentry_queue_test.rb diff --git a/sentry-ruby/test-app/sentry_queue_test.rb b/sentry-ruby/test-app/sentry_queue_test.rb deleted file mode 100644 index e76e355a9..000000000 --- a/sentry-ruby/test-app/sentry_queue_test.rb +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Sentry Queue Time Test Generator -# -# USAGE: -# export SENTRY_DSN='https://your-key@o123.ingest.us.sentry.io/456' -# export SENTRY_SKIP_SSL_VERIFY=true # if you get SSL errors -# ruby sentry_queue_test.rb [duration_minutes] [requests_per_minute] [pattern] -# -# EXAMPLES: -# ruby sentry_queue_test.rb # 5 min, realistic pattern -# ruby sentry_queue_test.rb 10 30 spike # 10 min, spike pattern -# ruby sentry_queue_test.rb 5 20 steady # 5 min, steady pattern -# -# PATTERNS: -# realistic - Business hours pattern (peak 9am-5pm) -# spike - Sudden traffic spikes -# degradation - Gradual performance decline -# recovery - System recovering after incident -# steady - Consistent baseline -# wave - Smooth sine wave - -require 'bundler/inline' - -gemfile do - source 'https://rubygems.org' - gem 'rack' - gem 'sentry-ruby', path: File.expand_path('../../sentry-ruby', __dir__) -end - -require 'rack' -require 'sentry-ruby' - -# Configuration -DURATION_MINUTES = (ARGV[0] || 5).to_i -REQUESTS_PER_MINUTE = (ARGV[1] || 20).to_i -PATTERN = (ARGV[2] || 'realistic').downcase - -# Transaction sequences - realistic API patterns -TRANSACTION_SEQUENCES = { - user_journey: [ - { path: '/api/products', weight: 0.4 }, - { path: '/api/products/:id', weight: 0.25 }, - { path: '/api/cart', weight: 0.2 }, - { path: '/api/orders', weight: 0.1 }, - { path: '/api/payment', weight: 0.05 } - ], - - admin: [ - { path: '/api/admin/auth', weight: 0.1 }, - { path: '/api/admin/dashboard', weight: 0.3 }, - { path: '/api/admin/users', weight: 0.25 }, - { path: '/api/admin/reports', weight: 0.2 }, - { path: '/api/admin/analytics', weight: 0.15 } - ], - - background: [ - { path: '/api/webhooks/stripe', weight: 0.3 }, - { path: '/api/jobs/email', weight: 0.25 }, - { path: '/api/jobs/export', weight: 0.2 }, - { path: '/api/jobs/cleanup', weight: 0.15 }, - { path: '/api/cron/daily', weight: 0.1 } - ] -} - -# Queue time patterns -PATTERNS = { - 'realistic' => lambda do |progress| - hour_of_day = (progress * 24) % 24 - if hour_of_day >= 9 && hour_of_day <= 17 - base = 40 + (Math.sin((hour_of_day - 9) / 8.0 * Math::PI) * 30) - else - base = 10 + rand * 10 - end - base + (rand * 20 - 10) - end, - - 'spike' => lambda do |progress| - spike_phase = (progress * 5) % 1 - spike_phase < 0.15 ? 150 + rand * 100 : 15 + rand * 20 - end, - - 'degradation' => lambda do |progress| - base = progress * 200 - base + (rand * 50 - 25) - end, - - 'recovery' => lambda do |progress| - base = (1 - progress) * 200 + 10 - base + (rand * 30 - 15) - end, - - 'steady' => lambda do |progress| - 30 + rand * 20 - end, - - 'wave' => lambda do |progress| - Math.sin(progress * Math::PI * 2) * 50 + 60 + (rand * 20 - 10) - end -} - -unless PATTERNS.key?(PATTERN) - puts "\nError: Unknown pattern '#{PATTERN}'" - puts "\nAvailable patterns: #{PATTERNS.keys.join(', ')}" - exit 1 -end - -# Sentry setup -unless ENV['SENTRY_DSN'] - puts "\nError: SENTRY_DSN environment variable not set" - puts "\nSet it with:" - puts " export SENTRY_DSN='https://your-key@o123.ingest.us.sentry.io/456'" - exit 1 -end - -skip_ssl = ENV['SENTRY_SKIP_SSL_VERIFY'] == 'true' - -Sentry.init do |config| - config.dsn = ENV['SENTRY_DSN'] - config.traces_sample_rate = 1.0 - config.capture_queue_time = true - config.transport.ssl_verification = !skip_ssl -end - -# App setup -app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } -middleware = Sentry::Rack::CaptureExceptions.new(app) - -# Helper functions -def weighted_random_endpoint(sequences) - all_endpoints = sequences.flat_map { |_name, endpoints| endpoints } - total_weight = all_endpoints.sum { |ep| ep[:weight] } - random = rand * total_weight - - cumulative = 0 - all_endpoints.each do |endpoint| - cumulative += endpoint[:weight] - return endpoint[:path] if random <= cumulative - end - - all_endpoints.last[:path] -end - -def progress_bar(current, total, width = 40) - percent = (current / total.to_f * 100).round(1) - filled = (current / total.to_f * width).round - bar = "█" * filled + "░" * (width - filled) - "[#{bar}] #{percent}%" -end - -def format_time(seconds) - seconds < 60 ? "#{seconds.round}s" : "#{(seconds / 60).round(1)}min" -end - -# Main execution -puts "\nSentry Queue Time Test" -puts "=" * 70 -puts "Pattern: #{PATTERN}" -puts "Duration: #{DURATION_MINUTES} minutes" -puts "Frequency: #{REQUESTS_PER_MINUTE} req/min" -puts "Total: #{DURATION_MINUTES * REQUESTS_PER_MINUTE} requests" -puts "\nStarting in 2 seconds... (Ctrl+C to cancel)" -sleep 2 - -interval_seconds = 60.0 / REQUESTS_PER_MINUTE -start_time = Time.now -end_time = start_time + (DURATION_MINUTES * 60) -request_num = 0 - -puts "\nGenerating transactions... (Ctrl+C to stop)\n" - -begin - while Time.now < end_time - request_num += 1 - elapsed_seconds = Time.now - start_time - elapsed_minutes = elapsed_seconds / 60.0 - progress = elapsed_minutes / DURATION_MINUTES.to_f - - # Generate queue time - queue_time_ms = PATTERNS[PATTERN].call(progress) - queue_time_ms = [[queue_time_ms, 1].max, 1000].min - - # Select endpoint - endpoint = weighted_random_endpoint(TRANSACTION_SEQUENCES) - - # Create request - request_start_time = Time.now.to_f - (queue_time_ms / 1000.0) - - env = Rack::MockRequest.env_for(endpoint) - env['HTTP_X_REQUEST_START'] = "t=#{request_start_time}" - env['REQUEST_METHOD'] = ['GET', 'POST', 'PUT', 'DELETE'].sample - - # Add Puma wait occasionally - if rand < 0.15 - puma_wait = (rand * 40).round - env['puma.request_body_wait'] = puma_wait - actual_queue = [queue_time_ms - puma_wait, 0].max - else - actual_queue = queue_time_ms - end - - # Send transaction - middleware.call(env) - - # Progress display - total_requests = DURATION_MINUTES * REQUESTS_PER_MINUTE - remaining_seconds = end_time - Time.now - - print "\r#{progress_bar(request_num, total_requests)} " - print "#{request_num}/#{total_requests} | " - print "Queue: #{actual_queue.round(1)}ms | " - print "Remaining: #{format_time(remaining_seconds)} " - $stdout.flush - - sleep interval_seconds - end - - print "\r" + " " * 100 + "\r" - -rescue Interrupt - print "\r" + " " * 100 + "\r" - puts "\nStopped by user" -end - -# Summary -duration = ((Time.now - start_time) / 60.0).round(2) - -puts "\n" + "=" * 70 -puts "Complete" -puts "=" * 70 -puts "\nDuration: #{duration} minutes" -puts "Requests: #{request_num}" -puts "Pattern: #{PATTERN}" From 13cd936f1d02addbc7666a8847ab40e3bc4b7af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 15:31:00 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=E2=8F=B0=F0=9F=91=AE=F0=9F=8F=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/sentry/rack/capture_exceptions_spec.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index 6f99ecdb2..e23204b89 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -686,14 +686,16 @@ def will_be_sampled_by_sdk end it "subtracts puma.request_body_wait" do - timestamp = Time.now.to_f - 0.1 # 100ms ago - env["HTTP_X_REQUEST_START"] = "t=#{timestamp}" - env["puma.request_body_wait"] = 40 # 40ms waiting for client + Timecop.freeze do + timestamp = Time.now.to_f - 0.1 # 100ms ago + env["HTTP_X_REQUEST_START"] = "t=#{timestamp}" + env["puma.request_body_wait"] = 40 # 40ms waiting for client - stack.call(env) + stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') - expect(queue_time).to be_within(10).of(60) # 100 - 40 + queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + expect(queue_time).to be_within(10).of(60) # 100 - 40 + end end it "handles different timestamp formats" do From b32132c12f9198b1ece17e72684adaa1441ec3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 15:50:02 +0100 Subject: [PATCH 11/12] change name --- sentry-ruby/lib/sentry/span.rb | 2 +- .../spec/sentry/rack/capture_exceptions_spec.rb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb index 7510c88dc..0d35e4fab 100644 --- a/sentry-ruby/lib/sentry/span.rb +++ b/sentry-ruby/lib/sentry/span.rb @@ -51,7 +51,7 @@ module DataConventions MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" # Time in ms the request spent in the server queue before processing began. - HTTP_QUEUE_TIME_MS = "http.queue_time_ms" + HTTP_QUEUE_TIME_MS = "http.server.request.time_in_queue" end STATUS_MAP = { diff --git a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb index e23204b89..a5671264e 100644 --- a/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb +++ b/sentry-ruby/spec/sentry/rack/capture_exceptions_spec.rb @@ -681,7 +681,7 @@ def will_be_sampled_by_sdk stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_within(10).of(50) end @@ -693,7 +693,7 @@ def will_be_sampled_by_sdk stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_within(10).of(60) # 100 - 40 end end @@ -705,7 +705,7 @@ def will_be_sampled_by_sdk stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_within(10).of(30) end end @@ -714,7 +714,7 @@ def will_be_sampled_by_sdk it "doesn't add queue time data" do stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_nil end end @@ -725,7 +725,7 @@ def will_be_sampled_by_sdk stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_nil end end @@ -741,7 +741,7 @@ def will_be_sampled_by_sdk stack.call(env) - queue_time = transaction.contexts.dig(:trace, :data, 'http.queue_time_ms') + queue_time = transaction.contexts.dig(:trace, :data, 'http.server.request.time_in_queue') expect(queue_time).to be_nil end end From 00cc056cbf7b445caa5d68497bca7be46a5e837e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Daxb=C3=B6ck?= Date: Thu, 15 Jan 2026 18:33:28 +0100 Subject: [PATCH 12/12] remove upper bounds --- sentry-ruby/lib/sentry/rack/capture_exceptions.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb index d729f89d8..239d9407f 100644 --- a/sentry-ruby/lib/sentry/rack/capture_exceptions.rb +++ b/sentry-ruby/lib/sentry/rack/capture_exceptions.rb @@ -115,8 +115,8 @@ def extract_queue_time(env) total_time_ms = ((Time.now.to_f - request_start) * 1000).round(2) - # reject negative (clock skew between proxy & app server) or very large values (> 60 seconds) - return nil unless total_time_ms >= 0 && total_time_ms < 60_000 + # reject negative (clock skew between proxy & app server) + return nil unless total_time_ms >= 0 puma_wait_ms = env["puma.request_body_wait"]