Skip to content

Propagated sampling rates #2671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ VERSION="3.4.5"

# E2E testing
SENTRY_DSN="http://user:[email protected]/project/42"
SENTRY_DSN_JS="http://user:[email protected]/project/43"
# SENTRY_DSN_JS="http://user:[email protected]/project/43"
SENTRY_DSN_JS=""

SENTRY_E2E_RAILS_APP_PORT=4000
SENTRY_E2E_SVELTE_APP_PORT=4001
Expand Down
7 changes: 6 additions & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libgdbm-dev \
sqlite3 \
libsqlite3-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash -

RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs \
npm \
chromium \
chromium-driver \
&& apt-get clean \
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/e2e_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

env:
DOCKER_IMAGE: "ghcr.io/getsentry/sentry-ruby-devcontainer-3.4"
DOCKER_TAG: "c7f73e278f0f8ad6035d578685e7bfb34be5eb4c"
DOCKER_TAG: "d54d0ea1ee3e0d49f2b86d2689278447ccbbe0f9"

steps:
- name: Checkout code
Expand All @@ -50,9 +50,9 @@ jobs:
uses: actions/cache@v3
with:
path: spec/apps/svelte-mini/node_modules
key: ${{ runner.os }}-node-modules-${{ hashFiles('spec/apps/svelte-mini/package-lock.json') }}
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('spec/apps/svelte-mini/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-modules-
${{ runner.os }}-${{ runner.arch }}-node-modules-

- name: Set up test container
run: |
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased

### Feature

- Propagated sampling rates as specified in [Traces](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value) docs ([#2671](https://github.com/getsentry/sentry-ruby/pull/2671))

### Internal

- Factor out do_request in HTTP transport ([#2662](https://github.com/getsentry/sentry-ruby/pull/2662))
Expand Down
1 change: 1 addition & 0 deletions sentry-ruby/lib/sentry-ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "sentry/utils/argument_checking_helper"
require "sentry/utils/encoding_helper"
require "sentry/utils/logging_helper"
require "sentry/utils/sample_rand"
require "sentry/configuration"
require "sentry/structured_logger"
require "sentry/event"
Expand Down
4 changes: 3 additions & 1 deletion sentry-ruby/lib/sentry/hub.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ def start_transaction(transaction: nil, custom_sampling_context: {}, instrumente

sampling_context = {
transaction_context: transaction.to_hash,
parent_sampled: transaction.parent_sampled
parent_sampled: transaction.parent_sampled,
parent_sample_rate: transaction.parent_sample_rate
}

sampling_context.merge!(custom_sampling_context)
Expand Down Expand Up @@ -357,6 +358,7 @@ def continue_trace(env, **options)
parent_span_id: propagation_context.parent_span_id,
parent_sampled: propagation_context.parent_sampled,
baggage: propagation_context.baggage,
sample_rand: propagation_context.sample_rand,
**options
)
end
Expand Down
64 changes: 50 additions & 14 deletions sentry-ruby/lib/sentry/propagation_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "securerandom"
require "sentry/baggage"
require "sentry/utils/uuid"
require "sentry/utils/sample_rand"

module Sentry
class PropagationContext
Expand Down Expand Up @@ -33,13 +34,58 @@ class PropagationContext
# Please use the #get_baggage method for interfacing outside this class.
# @return [Baggage, nil]
attr_reader :baggage
# The propagated random value used for sampling decisions.
# @return [Float, nil]
attr_reader :sample_rand

# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
#
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
# @return [Array, nil]
def self.extract_sentry_trace(sentry_trace)
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
return if match.nil?

trace_id, parent_span_id, sampled_flag = match[1..3]
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"

[trace_id, parent_span_id, parent_sampled]
end

def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
return unless baggage&.items

sample_rand_str = baggage.items["sample_rand"]
return unless sample_rand_str

generator = Utils::SampleRand.new(trace_id: trace_id)
generator.generate_from_value(sample_rand_str)
end

def self.generate_sample_rand(baggage, trace_id, parent_sampled)
generator = Utils::SampleRand.new(trace_id: trace_id)

if baggage&.items && !parent_sampled.nil?
sample_rate_str = baggage.items["sample_rate"]
sample_rate = sample_rate_str&.to_f

if sample_rate && !parent_sampled.nil?
generator.generate_from_sampling_decision(parent_sampled, sample_rate)
else
generator.generate_from_trace_id
end
else
generator.generate_from_trace_id
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Sample Rate Handling Bug

In generate_sample_rand, the parent_sampled check is redundant in the inner condition. More critically, using sample_rate directly in the if statement causes a 0.0 rate to be treated as false, which incorrectly falls back to trace ID generation instead of respecting an explicit 0.0 sample rate.

Fix in Cursor Fix in Web

end

def initialize(scope, env = nil)
@scope = scope
@parent_span_id = nil
@parent_sampled = nil
@baggage = nil
@incoming_trace = false
@sample_rand = nil

if env
sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
Expand All @@ -61,6 +107,8 @@ def initialize(scope, env = nil)
Baggage.new({})
end

@sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)

@baggage.freeze!
@incoming_trace = true
end
Expand All @@ -69,20 +117,7 @@ def initialize(scope, env = nil)

@trace_id ||= Utils.uuid
@span_id = Utils.uuid.slice(0, 16)
end

# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
#
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
# @return [Array, nil]
def self.extract_sentry_trace(sentry_trace)
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
return nil if match.nil?

trace_id, parent_span_id, sampled_flag = match[1..3]
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"

[trace_id, parent_span_id, parent_sampled]
@sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
end

# Returns the trace context that can be used to embed in an Event.
Expand Down Expand Up @@ -123,6 +158,7 @@ def populate_head_baggage

items = {
"trace_id" => trace_id,
"sample_rand" => Utils::SampleRand.format(@sample_rand),
"environment" => configuration.environment,
"release" => configuration.release,
"public_key" => configuration.dsn&.public_key
Expand Down
30 changes: 29 additions & 1 deletion sentry-ruby/lib/sentry/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require "sentry/baggage"
require "sentry/profiler"
require "sentry/utils/sample_rand"
require "sentry/propagation_context"

module Sentry
Expand Down Expand Up @@ -57,12 +58,17 @@ class Transaction < Span
# @return [Profiler]
attr_reader :profiler

# Sample rand value generated from trace_id
# @return [String]
attr_reader :sample_rand

def initialize(
hub:,
name: nil,
source: :custom,
parent_sampled: nil,
baggage: nil,
sample_rand: nil,
**options
)
super(transaction: self, **options)
Expand All @@ -82,12 +88,18 @@ def initialize(
@effective_sample_rate = nil
@contexts = {}
@measurements = {}
@sample_rand = sample_rand

unless @hub.profiler_running?
@profiler = @configuration.profiler_class.new(@configuration)
end

init_span_recorder

unless @sample_rand
generator = Utils::SampleRand.new(trace_id: @trace_id)
@sample_rand = generator.generate_from_trace_id
end
end

# @deprecated use Sentry.continue_trace instead.
Expand Down Expand Up @@ -123,12 +135,15 @@ def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_h

baggage.freeze!

sample_rand = extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)

new(
trace_id: trace_id,
parent_span_id: parent_span_id,
parent_sampled: parent_sampled,
hub: hub,
baggage: baggage,
sample_rand: sample_rand,
**options
)
end
Expand All @@ -139,6 +154,11 @@ def self.extract_sentry_trace(sentry_trace)
PropagationContext.extract_sentry_trace(sentry_trace)
end

def self.extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
PropagationContext.extract_sample_rand_from_baggage(baggage, trace_id) ||
PropagationContext.generate_sample_rand(baggage, trace_id, parent_sampled)
end

# @return [Hash]
def to_hash
hash = super
Expand All @@ -153,6 +173,13 @@ def to_hash
hash
end

def parent_sample_rate
return unless @baggage&.items

sample_rate_str = @baggage.items["sample_rate"]
sample_rate_str&.to_f
end

# @return [Transaction]
def deep_dup
copy = super
Expand Down Expand Up @@ -225,7 +252,7 @@ def set_initial_sample_decision(sampling_context:)
@effective_sample_rate /= 2**factor
end

@sampled = Random.rand < @effective_sample_rate
@sampled = @sample_rand < @effective_sample_rate
end

if @sampled
Expand Down Expand Up @@ -331,6 +358,7 @@ def populate_head_baggage
items = {
"trace_id" => trace_id,
"sample_rate" => effective_sample_rate&.to_s,
"sample_rand" => Utils::SampleRand.format(@sample_rand),
"sampled" => sampled&.to_s,
"environment" => @environment,
"release" => @release,
Expand Down
97 changes: 97 additions & 0 deletions sentry-ruby/lib/sentry/utils/sample_rand.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# frozen_string_literal: true

module Sentry
module Utils
class SampleRand
PRECISION = 1_000_000.0
FORMAT_PRECISION = 6

attr_reader :trace_id

def self.valid?(value)
return false unless value
value >= 0.0 && value < 1.0
end

def self.format(value)
return unless value

truncated = (value * PRECISION).floor / PRECISION
"%.#{FORMAT_PRECISION}f" % truncated
end

def initialize(trace_id: nil)
@trace_id = trace_id
end

def generate_from_trace_id
(random_from_trace_id * PRECISION).floor / PRECISION
end

def generate_from_sampling_decision(sampled, sample_rate)
if invalid_sample_rate?(sample_rate)
fallback_generation
else
generate_based_on_sampling(sampled, sample_rate)
end
end

def generate_from_value(sample_rand_value)
parsed_value = parse_value(sample_rand_value)

if self.class.valid?(parsed_value)
parsed_value
else
fallback_generation
end
end

private

def random_from_trace_id
if @trace_id
Random.new(@trace_id[0, 16].to_i(16))
else
Random.new
end.rand(1.0)
end

def invalid_sample_rate?(sample_rate)
sample_rate.nil? || sample_rate <= 0.0 || sample_rate > 1.0
end

def fallback_generation
if @trace_id
(random_from_trace_id * PRECISION).floor / PRECISION
else
format_random(Random.rand(1.0))
end
end

def generate_based_on_sampling(sampled, sample_rate)
random = random_from_trace_id

result = if sampled
random * sample_rate
elsif sample_rate == 1.0
random
else
sample_rate + random * (1.0 - sample_rate)
end

format_random(result)
end

def format_random(value)
truncated = (value * PRECISION).floor / PRECISION
("%.#{FORMAT_PRECISION}f" % truncated).to_f
end

def parse_value(sample_rand_value)
Float(sample_rand_value)
rescue ArgumentError
nil
end
end
end
end
1 change: 1 addition & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ def sentry_context
expect(event.dynamic_sampling_context).to eq({
"environment" => "development",
"public_key" => "12345",
"sample_rand" => Sentry::Utils::SampleRand.format(transaction.sample_rand),
"sample_rate" => "1.0",
"sampled" => "true",
"transaction" => "test transaction",
Expand Down
Loading
Loading