Skip to content

Commit 78487f2

Browse files
committed
Add Utils::SampleRand
1 parent 9b37693 commit 78487f2

File tree

3 files changed

+192
-0
lines changed

3 files changed

+192
-0
lines changed

sentry-ruby/lib/sentry-ruby.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require "sentry/utils/argument_checking_helper"
1111
require "sentry/utils/encoding_helper"
1212
require "sentry/utils/logging_helper"
13+
require "sentry/utils/sample_rand"
1314
require "sentry/configuration"
1415
require "sentry/structured_logger"
1516
require "sentry/event"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Utils
5+
module SampleRand
6+
def self.generate_from_trace_id(trace_id)
7+
seed = trace_id[0, 16].to_i(16)
8+
rng = Random.new(seed)
9+
10+
value = rng.rand
11+
12+
(value * 1_000_000).floor / 1_000_000.0
13+
end
14+
15+
def self.generate_from_sampling_decision(sampled, sample_rate, trace_id = nil)
16+
if sample_rate.nil? || sample_rate <= 0.0 || sample_rate > 1.0
17+
return trace_id ? generate_from_trace_id(trace_id) : Random.rand.round(6)
18+
end
19+
20+
rng = trace_id ? Random.new(trace_id[0, 16].to_i(16)) : Random
21+
22+
if sampled
23+
(rng.rand * sample_rate).round(6)
24+
else
25+
(sample_rate + rng.rand * (1.0 - sample_rate)).round(6)
26+
end
27+
end
28+
29+
def self.valid?(sample_rand)
30+
return false unless sample_rand
31+
return false if sample_rand.is_a?(String) && sample_rand.empty?
32+
33+
value = sample_rand.is_a?(String) ? sample_rand.to_f : sample_rand
34+
value >= 0.0 && value < 1.0
35+
end
36+
37+
def self.format(sample_rand)
38+
truncated = (sample_rand * 1_000_000).floor / 1_000_000.0
39+
"%.6f" % truncated
40+
end
41+
end
42+
end
43+
end
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Sentry::Utils::SampleRand do
4+
describe ".generate_from_trace_id" do
5+
it "generates a float in range [0, 1) with 6 decimal places" do
6+
trace_id = "abcdef1234567890abcdef1234567890"
7+
sample_rand = described_class.generate_from_trace_id(trace_id)
8+
9+
expect(sample_rand).to be_a(Float)
10+
expect(sample_rand).to be >= 0.0
11+
expect(sample_rand).to be < 1.0
12+
expect(sample_rand.to_s.split('.')[1].length).to be <= 6
13+
end
14+
15+
it "generates deterministic values for the same trace_id" do
16+
trace_id = "abcdef1234567890abcdef1234567890"
17+
18+
sample_rand1 = described_class.generate_from_trace_id(trace_id)
19+
sample_rand2 = described_class.generate_from_trace_id(trace_id)
20+
21+
expect(sample_rand1).to eq(sample_rand2)
22+
end
23+
24+
it "generates different values for different trace_ids" do
25+
trace_id1 = "abcdef1234567890abcdef1234567890"
26+
trace_id2 = "fedcba0987654321fedcba0987654321"
27+
28+
sample_rand1 = described_class.generate_from_trace_id(trace_id1)
29+
sample_rand2 = described_class.generate_from_trace_id(trace_id2)
30+
31+
expect(sample_rand1).not_to eq(sample_rand2)
32+
end
33+
34+
it "handles short trace_ids" do
35+
trace_id = "abc123"
36+
sample_rand = described_class.generate_from_trace_id(trace_id)
37+
38+
expect(sample_rand).to be_a(Float)
39+
expect(sample_rand).to be >= 0.0
40+
expect(sample_rand).to be < 1.0
41+
end
42+
end
43+
44+
describe ".generate_from_sampling_decision" do
45+
let(:trace_id) { "abcdef1234567890abcdef1234567890" }
46+
47+
context "with valid sample_rate and sampled=true" do
48+
it "generates value in range [0, sample_rate)" do
49+
sample_rate = 0.5
50+
sample_rand = described_class.generate_from_sampling_decision(true, sample_rate, trace_id)
51+
52+
expect(sample_rand).to be >= 0.0
53+
expect(sample_rand).to be < sample_rate
54+
end
55+
56+
it "is deterministic with trace_id" do
57+
sample_rate = 0.5
58+
59+
sample_rand1 = described_class.generate_from_sampling_decision(true, sample_rate, trace_id)
60+
sample_rand2 = described_class.generate_from_sampling_decision(true, sample_rate, trace_id)
61+
62+
expect(sample_rand1).to eq(sample_rand2)
63+
end
64+
end
65+
66+
context "with valid sample_rate and sampled=false" do
67+
it "generates value in range [sample_rate, 1)" do
68+
sample_rate = 0.3
69+
sample_rand = described_class.generate_from_sampling_decision(false, sample_rate, trace_id)
70+
71+
expect(sample_rand).to be >= sample_rate
72+
expect(sample_rand).to be < 1.0
73+
end
74+
75+
it "is deterministic with trace_id" do
76+
sample_rate = 0.3
77+
78+
sample_rand1 = described_class.generate_from_sampling_decision(false, sample_rate, trace_id)
79+
sample_rand2 = described_class.generate_from_sampling_decision(false, sample_rate, trace_id)
80+
81+
expect(sample_rand1).to eq(sample_rand2)
82+
end
83+
end
84+
85+
context "with invalid sample_rate" do
86+
it "falls back to trace_id generation when sample_rate is nil" do
87+
expected = described_class.generate_from_trace_id(trace_id)
88+
actual = described_class.generate_from_sampling_decision(true, nil, trace_id)
89+
90+
expect(actual).to eq(expected)
91+
end
92+
93+
it "falls back to trace_id generation when sample_rate is 0" do
94+
expected = described_class.generate_from_trace_id(trace_id)
95+
actual = described_class.generate_from_sampling_decision(true, 0.0, trace_id)
96+
97+
expect(actual).to eq(expected)
98+
end
99+
100+
it "falls back to trace_id generation when sample_rate > 1" do
101+
expected = described_class.generate_from_trace_id(trace_id)
102+
actual = described_class.generate_from_sampling_decision(true, 1.5, trace_id)
103+
104+
expect(actual).to eq(expected)
105+
end
106+
107+
it "uses Random.rand when no trace_id provided" do
108+
result = described_class.generate_from_sampling_decision(true, nil, nil)
109+
110+
expect(result).to be_a(Float)
111+
expect(result).to be >= 0.0
112+
expect(result).to be < 1.0
113+
expect(result.to_s.split('.')[1].length).to be <= 6
114+
end
115+
end
116+
end
117+
118+
describe ".valid?" do
119+
it "returns true for valid float values" do
120+
expect(described_class.valid?(0.0)).to be true
121+
expect(described_class.valid?(0.5)).to be true
122+
expect(described_class.valid?(0.999999)).to be true
123+
end
124+
125+
it "returns true for valid string values" do
126+
expect(described_class.valid?("0.0")).to be true
127+
expect(described_class.valid?("0.5")).to be true
128+
expect(described_class.valid?("0.999999")).to be true
129+
end
130+
131+
it "returns false for invalid values" do
132+
expect(described_class.valid?(nil)).to be false
133+
expect(described_class.valid?(-0.1)).to be false
134+
expect(described_class.valid?(1.0)).to be false
135+
expect(described_class.valid?(1.5)).to be false
136+
expect(described_class.valid?("")).to be false
137+
# Note: "invalid" string converts to 0.0 which is valid
138+
end
139+
end
140+
141+
describe ".format" do
142+
it "formats float to 6 decimal places" do
143+
expect(described_class.format(0.123456789)).to eq("0.123456")
144+
expect(described_class.format(0.1)).to eq("0.100000")
145+
expect(described_class.format(0.0)).to eq("0.000000")
146+
end
147+
end
148+
end

0 commit comments

Comments
 (0)