Skip to content

Commit 3e5a7b0

Browse files
committed
Add sample_rand to propagation context
1 parent 31c6187 commit 3e5a7b0

File tree

3 files changed

+230
-0
lines changed

3 files changed

+230
-0
lines changed

sentry-ruby/lib/sentry/propagation_context.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "securerandom"
44
require "sentry/baggage"
55
require "sentry/utils/uuid"
6+
require "sentry/utils/sample_rand"
67

78
module Sentry
89
class PropagationContext
@@ -33,13 +34,17 @@ class PropagationContext
3334
# Please use the #get_baggage method for interfacing outside this class.
3435
# @return [Baggage, nil]
3536
attr_reader :baggage
37+
# The propagated random value used for sampling decisions.
38+
# @return [Float, nil]
39+
attr_reader :sample_rand
3640

3741
def initialize(scope, env = nil)
3842
@scope = scope
3943
@parent_span_id = nil
4044
@parent_sampled = nil
4145
@baggage = nil
4246
@incoming_trace = false
47+
@sample_rand = nil
4348

4449
if env
4550
sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
@@ -61,6 +66,7 @@ def initialize(scope, env = nil)
6166
Baggage.new({})
6267
end
6368

69+
@sample_rand = extract_sample_rand_from_baggage(@baggage)
6470
@baggage.freeze!
6571
@incoming_trace = true
6672
end
@@ -69,6 +75,7 @@ def initialize(scope, env = nil)
6975

7076
@trace_id ||= Utils.uuid
7177
@span_id = Utils.uuid.slice(0, 16)
78+
@sample_rand ||= generate_sample_rand
7279
end
7380

7481
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
@@ -123,6 +130,7 @@ def populate_head_baggage
123130

124131
items = {
125132
"trace_id" => trace_id,
133+
"sample_rand" => Utils::SampleRand.format(@sample_rand),
126134
"environment" => configuration.environment,
127135
"release" => configuration.release,
128136
"public_key" => configuration.dsn&.public_key
@@ -131,5 +139,30 @@ def populate_head_baggage
131139
items.compact!
132140
@baggage = Baggage.new(items, mutable: false)
133141
end
142+
143+
def extract_sample_rand_from_baggage(baggage)
144+
return nil unless baggage&.items
145+
146+
sample_rand_str = baggage.items["sample_rand"]
147+
return nil unless sample_rand_str
148+
149+
sample_rand = sample_rand_str.to_f
150+
Utils::SampleRand.valid?(sample_rand) ? sample_rand : nil
151+
end
152+
153+
def generate_sample_rand
154+
if @incoming_trace && @parent_sampled && @baggage
155+
sample_rate_str = @baggage.items["sample_rate"]
156+
sample_rate = sample_rate_str&.to_f
157+
158+
if sample_rate && @parent_sampled != nil
159+
Utils::SampleRand.generate_from_sampling_decision(@parent_sampled, sample_rate, @trace_id)
160+
else
161+
Utils::SampleRand.generate_from_trace_id(@trace_id)
162+
end
163+
else
164+
Utils::SampleRand.generate_from_trace_id(@trace_id)
165+
end
166+
end
134167
end
135168
end
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Sentry::PropagationContext do
4+
before do
5+
perform_basic_setup
6+
end
7+
8+
let(:scope) { Sentry.get_current_scope }
9+
10+
describe "sample_rand integration" do
11+
describe "#initialize" do
12+
it "generates sample_rand when no incoming trace" do
13+
context = described_class.new(scope)
14+
15+
expect(context.sample_rand).to be_a(Float)
16+
expect(context.sample_rand).to be >= 0.0
17+
expect(context.sample_rand).to be < 1.0
18+
end
19+
20+
it "generates deterministic sample_rand from trace_id" do
21+
context1 = described_class.new(scope)
22+
context2 = described_class.new(scope)
23+
24+
# Different contexts should have different trace_ids and thus different sample_rand
25+
expect(context1.sample_rand).not_to eq(context2.sample_rand)
26+
27+
# But same trace_id should generate same sample_rand
28+
trace_id = context1.trace_id
29+
allow(Sentry::Utils).to receive(:uuid).and_return(trace_id)
30+
context3 = described_class.new(scope)
31+
32+
expect(context3.sample_rand).to eq(context1.sample_rand)
33+
end
34+
35+
context "with incoming trace" do
36+
let(:env) do
37+
{
38+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
39+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rand=0.123456"
40+
}
41+
end
42+
43+
it "uses sample_rand from incoming baggage" do
44+
context = described_class.new(scope, env)
45+
46+
expect(context.sample_rand).to eq(0.123456)
47+
expect(context.incoming_trace).to be true
48+
end
49+
end
50+
51+
context "with incoming trace but no sample_rand in baggage" do
52+
let(:env) do
53+
{
54+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
55+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rate=0.5"
56+
}
57+
end
58+
59+
it "generates sample_rand based on sampling decision" do
60+
context = described_class.new(scope, env)
61+
62+
expect(context.sample_rand).to be_a(Float)
63+
expect(context.sample_rand).to be >= 0.0
64+
expect(context.sample_rand).to be < 1.0
65+
expect(context.incoming_trace).to be true
66+
67+
# For sampled=true and sample_rate=0.5, sample_rand should be < 0.5
68+
expect(context.sample_rand).to be < 0.5
69+
end
70+
71+
it "is deterministic for same trace" do
72+
context1 = described_class.new(scope, env)
73+
context2 = described_class.new(scope, env)
74+
75+
expect(context1.sample_rand).to eq(context2.sample_rand)
76+
end
77+
end
78+
79+
context "with incoming trace but no baggage" do
80+
let(:env) do
81+
{
82+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1"
83+
}
84+
end
85+
86+
it "generates deterministic sample_rand from trace_id" do
87+
context = described_class.new(scope, env)
88+
89+
expect(context.sample_rand).to be_a(Float)
90+
expect(context.sample_rand).to be >= 0.0
91+
expect(context.sample_rand).to be < 1.0
92+
expect(context.incoming_trace).to be true
93+
94+
# Should be deterministic based on trace_id
95+
expected = Sentry::Utils::SampleRand.generate_from_trace_id("771a43a4192642f0b136d5159a501700")
96+
expect(context.sample_rand).to eq(expected)
97+
end
98+
end
99+
end
100+
101+
describe "#get_baggage" do
102+
it "includes sample_rand in baggage" do
103+
context = described_class.new(scope)
104+
baggage = context.get_baggage
105+
106+
expect(baggage.items["sample_rand"]).to eq(Sentry::Utils::SampleRand.format(context.sample_rand))
107+
end
108+
109+
context "with incoming baggage containing sample_rand" do
110+
let(:env) do
111+
{
112+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
113+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rand=0.654321"
114+
}
115+
end
116+
117+
it "preserves incoming sample_rand in baggage" do
118+
context = described_class.new(scope, env)
119+
baggage = context.get_baggage
120+
121+
expect(baggage.items["sample_rand"]).to eq("0.654321")
122+
end
123+
end
124+
end
125+
126+
describe "extract_sample_rand_from_baggage" do
127+
it "extracts valid sample_rand from baggage" do
128+
baggage = Sentry::Baggage.new({ "sample_rand" => "0.123456" })
129+
context = described_class.new(scope)
130+
131+
sample_rand = context.send(:extract_sample_rand_from_baggage, baggage)
132+
expect(sample_rand).to eq(0.123456)
133+
end
134+
135+
it "returns nil for invalid sample_rand" do
136+
baggage = Sentry::Baggage.new({ "sample_rand" => "1.5" }) # > 1.0 is invalid
137+
context = described_class.new(scope)
138+
139+
sample_rand = context.send(:extract_sample_rand_from_baggage, baggage)
140+
expect(sample_rand).to be_nil
141+
end
142+
143+
it "returns nil when no sample_rand in baggage" do
144+
baggage = Sentry::Baggage.new({ "trace_id" => "abc123" })
145+
context = described_class.new(scope)
146+
147+
sample_rand = context.send(:extract_sample_rand_from_baggage, baggage)
148+
expect(sample_rand).to be_nil
149+
end
150+
151+
it "returns nil when baggage is nil" do
152+
context = described_class.new(scope)
153+
154+
sample_rand = context.send(:extract_sample_rand_from_baggage, nil)
155+
expect(sample_rand).to be_nil
156+
end
157+
end
158+
159+
describe "generate_sample_rand" do
160+
context "with incoming trace and sampling decision" do
161+
let(:context) do
162+
ctx = described_class.new(scope)
163+
ctx.instance_variable_set(:@incoming_trace, true)
164+
ctx.instance_variable_set(:@parent_sampled, true)
165+
ctx.instance_variable_set(:@baggage, Sentry::Baggage.new({ "sample_rate" => "0.5" }))
166+
ctx.instance_variable_set(:@trace_id, "771a43a4192642f0b136d5159a501700")
167+
ctx
168+
end
169+
170+
it "generates sample_rand based on sampling decision" do
171+
sample_rand = context.send(:generate_sample_rand)
172+
173+
expect(sample_rand).to be_a(Float)
174+
expect(sample_rand).to be >= 0.0
175+
expect(sample_rand).to be < 0.5
176+
end
177+
end
178+
179+
context "without incoming trace" do
180+
let(:context) do
181+
ctx = described_class.new(scope)
182+
ctx.instance_variable_set(:@incoming_trace, false)
183+
ctx.instance_variable_set(:@trace_id, "771a43a4192642f0b136d5159a501700")
184+
ctx
185+
end
186+
187+
it "generates deterministic sample_rand from trace_id" do
188+
sample_rand = context.send(:generate_sample_rand)
189+
expected = Sentry::Utils::SampleRand.generate_from_trace_id("771a43a4192642f0b136d5159a501700")
190+
191+
expect(sample_rand).to eq(expected)
192+
end
193+
end
194+
end
195+
end
196+
end

sentry-ruby/spec/sentry/propagation_context_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
expect(baggage.mutable).to eq(false)
120120
expect(baggage.items).to eq({
121121
"trace_id" => subject.trace_id,
122+
"sample_rand" => Sentry::Utils::SampleRand.format(subject.sample_rand),
122123
"environment" => "test",
123124
"release" => "foobar",
124125
"public_key" => Sentry.configuration.dsn.public_key

0 commit comments

Comments
 (0)