Skip to content

Commit 9eed7ae

Browse files
committed
Add sample_rand to propagation context
1 parent 78487f2 commit 9eed7ae

File tree

3 files changed

+195
-1
lines changed

3 files changed

+195
-1
lines changed

sentry-ruby/lib/sentry/propagation_context.rb

Lines changed: 34 additions & 1 deletion
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.
@@ -77,7 +84,7 @@ def initialize(scope, env = nil)
7784
# @return [Array, nil]
7885
def self.extract_sentry_trace(sentry_trace)
7986
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
80-
return nil if match.nil?
87+
return if match.nil?
8188

8289
trace_id, parent_span_id, sampled_flag = match[1..3]
8390
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
@@ -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 unless baggage&.items
145+
146+
sample_rand_str = baggage.items["sample_rand"]
147+
return 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.nil? && @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: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
expect(context1.sample_rand).not_to eq(context2.sample_rand)
25+
26+
trace_id = context1.trace_id
27+
allow(Sentry::Utils).to receive(:uuid).and_return(trace_id)
28+
context3 = described_class.new(scope)
29+
30+
expect(context3.sample_rand).to eq(context1.sample_rand)
31+
end
32+
33+
context "with incoming trace" do
34+
let(:env) do
35+
{
36+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
37+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rand=0.123456"
38+
}
39+
end
40+
41+
it "uses sample_rand from incoming baggage" do
42+
context = described_class.new(scope, env)
43+
44+
expect(context.sample_rand).to eq(0.123456)
45+
expect(context.incoming_trace).to be true
46+
end
47+
end
48+
49+
context "with incoming trace but no sample_rand in baggage" do
50+
let(:env) do
51+
{
52+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
53+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rate=0.5"
54+
}
55+
end
56+
57+
it "generates sample_rand based on sampling decision" do
58+
context = described_class.new(scope, env)
59+
60+
expect(context.sample_rand).to be_a(Float)
61+
expect(context.sample_rand).to be >= 0.0
62+
expect(context.sample_rand).to be < 1.0
63+
expect(context.incoming_trace).to be true
64+
65+
expect(context.sample_rand).to be < 0.5
66+
end
67+
68+
it "is deterministic for same trace" do
69+
context1 = described_class.new(scope, env)
70+
context2 = described_class.new(scope, env)
71+
72+
expect(context1.sample_rand).to eq(context2.sample_rand)
73+
end
74+
end
75+
76+
context "with incoming trace and parent_sampled=false" do
77+
let(:env) do
78+
{
79+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-0",
80+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rate=0.5"
81+
}
82+
end
83+
84+
it "generates sample_rand based on unsampled decision" do
85+
context = described_class.new(scope, env)
86+
87+
expect(context.sample_rand).to be_a(Float)
88+
expect(context.sample_rand).to be >= 0.0
89+
expect(context.sample_rand).to be < 1.0
90+
expect(context.incoming_trace).to be true
91+
expect(context.parent_sampled).to be false
92+
93+
expect(context.sample_rand).to be >= 0.5
94+
end
95+
96+
it "is deterministic for same trace" do
97+
context1 = described_class.new(scope, env)
98+
context2 = described_class.new(scope, env)
99+
100+
expect(context1.sample_rand).to eq(context2.sample_rand)
101+
end
102+
103+
it "uses parent's explicit unsampled decision instead of falling back to trace_id generation" do
104+
context = described_class.new(scope, env)
105+
106+
expected_from_decision = Sentry::Utils::SampleRand.generate_from_sampling_decision(false, 0.5, "771a43a4192642f0b136d5159a501700")
107+
expected_from_trace_id = Sentry::Utils::SampleRand.generate_from_trace_id("771a43a4192642f0b136d5159a501700")
108+
109+
expect(context.sample_rand).to eq(expected_from_decision)
110+
expect(context.sample_rand).not_to eq(expected_from_trace_id)
111+
end
112+
end
113+
114+
context "with incoming trace but no baggage" do
115+
let(:env) do
116+
{
117+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1"
118+
}
119+
end
120+
121+
it "generates deterministic sample_rand from trace_id" do
122+
context = described_class.new(scope, env)
123+
124+
expect(context.sample_rand).to be_a(Float)
125+
expect(context.sample_rand).to be >= 0.0
126+
expect(context.sample_rand).to be < 1.0
127+
expect(context.incoming_trace).to be true
128+
129+
expected = Sentry::Utils::SampleRand.generate_from_trace_id("771a43a4192642f0b136d5159a501700")
130+
expect(context.sample_rand).to eq(expected)
131+
end
132+
end
133+
end
134+
135+
describe "#get_baggage" do
136+
it "includes sample_rand in baggage" do
137+
context = described_class.new(scope)
138+
baggage = context.get_baggage
139+
140+
expect(baggage.items["sample_rand"]).to eq(Sentry::Utils::SampleRand.format(context.sample_rand))
141+
end
142+
143+
context "with incoming baggage containing sample_rand" do
144+
let(:env) do
145+
{
146+
"HTTP_SENTRY_TRACE" => "771a43a4192642f0b136d5159a501700-7c51afd529da4a2a-1",
147+
"HTTP_BAGGAGE" => "sentry-trace_id=771a43a4192642f0b136d5159a501700,sentry-sample_rand=0.654321"
148+
}
149+
end
150+
151+
it "preserves incoming sample_rand in baggage" do
152+
context = described_class.new(scope, env)
153+
baggage = context.get_baggage
154+
155+
expect(baggage.items["sample_rand"]).to eq("0.654321")
156+
end
157+
end
158+
end
159+
end
160+
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)