Skip to content

Commit 53f3b58

Browse files
authored
Propagated sampling rates (#2671)
* Add Utils::SampleRand * Add sample_rand to propagation context * Add sample_rand to transaction * Add parent_sampled_rate to sampling context * Turn SampleRand into a class * More tests * Reduce duplication between Transaction/PropagationContext * Improve spec descriptions * Remove a spec that made no sense * Refine a couple of specs * Improve spec descriptions * Stricter parsing of the sample rand value * [devcontainer] update node * Update e2e workflow * Add more e2e coverage for DSC * Update svelte mini app for e2e * Update CHANGELOG * Improve spec * [e2e] clear logged events * Improve e2e spec
1 parent dd8b418 commit 53f3b58

29 files changed

+1938
-581
lines changed

.devcontainer/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ VERSION="3.4.5"
66

77
# E2E testing
88
SENTRY_DSN="http://user:[email protected]/project/42"
9-
SENTRY_DSN_JS="http://user:[email protected]/project/43"
9+
# SENTRY_DSN_JS="http://user:[email protected]/project/43"
10+
SENTRY_DSN_JS=""
1011

1112
SENTRY_E2E_RAILS_APP_PORT=4000
1213
SENTRY_E2E_SVELTE_APP_PORT=4001

.devcontainer/Dockerfile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2121
libgdbm-dev \
2222
sqlite3 \
2323
libsqlite3-dev \
24+
&& apt-get clean \
25+
&& rm -rf /var/lib/apt/lists/*
26+
27+
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
28+
29+
RUN apt-get update && apt-get install -y --no-install-recommends \
2430
nodejs \
25-
npm \
2631
chromium \
2732
chromium-driver \
2833
&& apt-get clean \

.github/workflows/e2e_tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727

2828
env:
2929
DOCKER_IMAGE: "ghcr.io/getsentry/sentry-ruby-devcontainer-3.4"
30-
DOCKER_TAG: "c7f73e278f0f8ad6035d578685e7bfb34be5eb4c"
30+
DOCKER_TAG: "d54d0ea1ee3e0d49f2b86d2689278447ccbbe0f9"
3131

3232
steps:
3333
- name: Checkout code
@@ -50,9 +50,9 @@ jobs:
5050
uses: actions/cache@v3
5151
with:
5252
path: spec/apps/svelte-mini/node_modules
53-
key: ${{ runner.os }}-node-modules-${{ hashFiles('spec/apps/svelte-mini/package-lock.json') }}
53+
key: ${{ runner.os }}-${{ runner.arch }}-node-modules-${{ hashFiles('spec/apps/svelte-mini/package-lock.json') }}
5454
restore-keys: |
55-
${{ runner.os }}-node-modules-
55+
${{ runner.os }}-${{ runner.arch }}-node-modules-
5656
5757
- name: Set up test container
5858
run: |

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Unreleased
22

3+
### Feature
4+
5+
- 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))
6+
37
### Internal
48

59
- Factor out do_request in HTTP transport ([#2662](https://github.com/getsentry/sentry-ruby/pull/2662))

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"

sentry-ruby/lib/sentry/hub.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ def start_transaction(transaction: nil, custom_sampling_context: {}, instrumente
120120

121121
sampling_context = {
122122
transaction_context: transaction.to_hash,
123-
parent_sampled: transaction.parent_sampled
123+
parent_sampled: transaction.parent_sampled,
124+
parent_sample_rate: transaction.parent_sample_rate
124125
}
125126

126127
sampling_context.merge!(custom_sampling_context)
@@ -357,6 +358,7 @@ def continue_trace(env, **options)
357358
parent_span_id: propagation_context.parent_span_id,
358359
parent_sampled: propagation_context.parent_sampled,
359360
baggage: propagation_context.baggage,
361+
sample_rand: propagation_context.sample_rand,
360362
**options
361363
)
362364
end

sentry-ruby/lib/sentry/propagation_context.rb

Lines changed: 50 additions & 14 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,58 @@ 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
40+
41+
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
42+
#
43+
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
44+
# @return [Array, nil]
45+
def self.extract_sentry_trace(sentry_trace)
46+
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
47+
return if match.nil?
48+
49+
trace_id, parent_span_id, sampled_flag = match[1..3]
50+
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
51+
52+
[trace_id, parent_span_id, parent_sampled]
53+
end
54+
55+
def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
56+
return unless baggage&.items
57+
58+
sample_rand_str = baggage.items["sample_rand"]
59+
return unless sample_rand_str
60+
61+
generator = Utils::SampleRand.new(trace_id: trace_id)
62+
generator.generate_from_value(sample_rand_str)
63+
end
64+
65+
def self.generate_sample_rand(baggage, trace_id, parent_sampled)
66+
generator = Utils::SampleRand.new(trace_id: trace_id)
67+
68+
if baggage&.items && !parent_sampled.nil?
69+
sample_rate_str = baggage.items["sample_rate"]
70+
sample_rate = sample_rate_str&.to_f
71+
72+
if sample_rate && !parent_sampled.nil?
73+
generator.generate_from_sampling_decision(parent_sampled, sample_rate)
74+
else
75+
generator.generate_from_trace_id
76+
end
77+
else
78+
generator.generate_from_trace_id
79+
end
80+
end
3681

3782
def initialize(scope, env = nil)
3883
@scope = scope
3984
@parent_span_id = nil
4085
@parent_sampled = nil
4186
@baggage = nil
4287
@incoming_trace = false
88+
@sample_rand = nil
4389

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

110+
@sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
111+
64112
@baggage.freeze!
65113
@incoming_trace = true
66114
end
@@ -69,20 +117,7 @@ def initialize(scope, env = nil)
69117

70118
@trace_id ||= Utils.uuid
71119
@span_id = Utils.uuid.slice(0, 16)
72-
end
73-
74-
# Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
75-
#
76-
# @param sentry_trace [String] the sentry-trace header value from the previous transaction.
77-
# @return [Array, nil]
78-
def self.extract_sentry_trace(sentry_trace)
79-
match = SENTRY_TRACE_REGEXP.match(sentry_trace)
80-
return nil if match.nil?
81-
82-
trace_id, parent_span_id, sampled_flag = match[1..3]
83-
parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"
84-
85-
[trace_id, parent_span_id, parent_sampled]
120+
@sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
86121
end
87122

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

124159
items = {
125160
"trace_id" => trace_id,
161+
"sample_rand" => Utils::SampleRand.format(@sample_rand),
126162
"environment" => configuration.environment,
127163
"release" => configuration.release,
128164
"public_key" => configuration.dsn&.public_key

sentry-ruby/lib/sentry/transaction.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require "sentry/baggage"
44
require "sentry/profiler"
5+
require "sentry/utils/sample_rand"
56
require "sentry/propagation_context"
67

78
module Sentry
@@ -57,12 +58,17 @@ class Transaction < Span
5758
# @return [Profiler]
5859
attr_reader :profiler
5960

61+
# Sample rand value generated from trace_id
62+
# @return [String]
63+
attr_reader :sample_rand
64+
6065
def initialize(
6166
hub:,
6267
name: nil,
6368
source: :custom,
6469
parent_sampled: nil,
6570
baggage: nil,
71+
sample_rand: nil,
6672
**options
6773
)
6874
super(transaction: self, **options)
@@ -82,12 +88,18 @@ def initialize(
8288
@effective_sample_rate = nil
8389
@contexts = {}
8490
@measurements = {}
91+
@sample_rand = sample_rand
8592

8693
unless @hub.profiler_running?
8794
@profiler = @configuration.profiler_class.new(@configuration)
8895
end
8996

9097
init_span_recorder
98+
99+
unless @sample_rand
100+
generator = Utils::SampleRand.new(trace_id: @trace_id)
101+
@sample_rand = generator.generate_from_trace_id
102+
end
91103
end
92104

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

124136
baggage.freeze!
125137

138+
sample_rand = extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
139+
126140
new(
127141
trace_id: trace_id,
128142
parent_span_id: parent_span_id,
129143
parent_sampled: parent_sampled,
130144
hub: hub,
131145
baggage: baggage,
146+
sample_rand: sample_rand,
132147
**options
133148
)
134149
end
@@ -139,6 +154,11 @@ def self.extract_sentry_trace(sentry_trace)
139154
PropagationContext.extract_sentry_trace(sentry_trace)
140155
end
141156

157+
def self.extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
158+
PropagationContext.extract_sample_rand_from_baggage(baggage, trace_id) ||
159+
PropagationContext.generate_sample_rand(baggage, trace_id, parent_sampled)
160+
end
161+
142162
# @return [Hash]
143163
def to_hash
144164
hash = super
@@ -153,6 +173,13 @@ def to_hash
153173
hash
154174
end
155175

176+
def parent_sample_rate
177+
return unless @baggage&.items
178+
179+
sample_rate_str = @baggage.items["sample_rate"]
180+
sample_rate_str&.to_f
181+
end
182+
156183
# @return [Transaction]
157184
def deep_dup
158185
copy = super
@@ -225,7 +252,7 @@ def set_initial_sample_decision(sampling_context:)
225252
@effective_sample_rate /= 2**factor
226253
end
227254

228-
@sampled = Random.rand < @effective_sample_rate
255+
@sampled = @sample_rand < @effective_sample_rate
229256
end
230257

231258
if @sampled
@@ -331,6 +358,7 @@ def populate_head_baggage
331358
items = {
332359
"trace_id" => trace_id,
333360
"sample_rate" => effective_sample_rate&.to_s,
361+
"sample_rand" => Utils::SampleRand.format(@sample_rand),
334362
"sampled" => sampled&.to_s,
335363
"environment" => @environment,
336364
"release" => @release,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Utils
5+
class SampleRand
6+
PRECISION = 1_000_000.0
7+
FORMAT_PRECISION = 6
8+
9+
attr_reader :trace_id
10+
11+
def self.valid?(value)
12+
return false unless value
13+
value >= 0.0 && value < 1.0
14+
end
15+
16+
def self.format(value)
17+
return unless value
18+
19+
truncated = (value * PRECISION).floor / PRECISION
20+
"%.#{FORMAT_PRECISION}f" % truncated
21+
end
22+
23+
def initialize(trace_id: nil)
24+
@trace_id = trace_id
25+
end
26+
27+
def generate_from_trace_id
28+
(random_from_trace_id * PRECISION).floor / PRECISION
29+
end
30+
31+
def generate_from_sampling_decision(sampled, sample_rate)
32+
if invalid_sample_rate?(sample_rate)
33+
fallback_generation
34+
else
35+
generate_based_on_sampling(sampled, sample_rate)
36+
end
37+
end
38+
39+
def generate_from_value(sample_rand_value)
40+
parsed_value = parse_value(sample_rand_value)
41+
42+
if self.class.valid?(parsed_value)
43+
parsed_value
44+
else
45+
fallback_generation
46+
end
47+
end
48+
49+
private
50+
51+
def random_from_trace_id
52+
if @trace_id
53+
Random.new(@trace_id[0, 16].to_i(16))
54+
else
55+
Random.new
56+
end.rand(1.0)
57+
end
58+
59+
def invalid_sample_rate?(sample_rate)
60+
sample_rate.nil? || sample_rate <= 0.0 || sample_rate > 1.0
61+
end
62+
63+
def fallback_generation
64+
if @trace_id
65+
(random_from_trace_id * PRECISION).floor / PRECISION
66+
else
67+
format_random(Random.rand(1.0))
68+
end
69+
end
70+
71+
def generate_based_on_sampling(sampled, sample_rate)
72+
random = random_from_trace_id
73+
74+
result = if sampled
75+
random * sample_rate
76+
elsif sample_rate == 1.0
77+
random
78+
else
79+
sample_rate + random * (1.0 - sample_rate)
80+
end
81+
82+
format_random(result)
83+
end
84+
85+
def format_random(value)
86+
truncated = (value * PRECISION).floor / PRECISION
87+
("%.#{FORMAT_PRECISION}f" % truncated).to_f
88+
end
89+
90+
def parse_value(sample_rand_value)
91+
Float(sample_rand_value)
92+
rescue ArgumentError
93+
nil
94+
end
95+
end
96+
end
97+
end

sentry-ruby/spec/sentry/client_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def sentry_context
273273
expect(event.dynamic_sampling_context).to eq({
274274
"environment" => "development",
275275
"public_key" => "12345",
276+
"sample_rand" => Sentry::Utils::SampleRand.format(transaction.sample_rand),
276277
"sample_rate" => "1.0",
277278
"sampled" => "true",
278279
"transaction" => "test transaction",

0 commit comments

Comments
 (0)