Skip to content

Commit c3d6d12

Browse files
feat(tracing): Use sample_rand for sampling decisions
Use the `sample_rand` value from an incoming trace to make sampling decisions, rather than generating a random value. When we are the head SDK starting a new trace, save our randomly-generated value as the `sample_rand`, and also change the random generation logic so that the `sample_rand` is computed deterministically based on the `trace_id`. Closes #3998
1 parent af64b16 commit c3d6d12

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

sentry_sdk/tracing.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import uuid
2-
import random
32
import warnings
43
from datetime import datetime, timedelta, timezone
4+
from random import Random
55

66
import sentry_sdk
77
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
@@ -774,6 +774,7 @@ class Transaction(Span):
774774
"_contexts",
775775
"_profile",
776776
"_baggage",
777+
"_sample_rand",
777778
)
778779

779780
def __init__( # type: ignore[misc]
@@ -799,6 +800,14 @@ def __init__( # type: ignore[misc]
799800
) # type: Optional[sentry_sdk.profiler.transaction_profiler.Profile]
800801
self._baggage = baggage
801802

803+
baggage_sample_rand = (
804+
None if self._baggage is None else self._baggage._sample_rand()
805+
)
806+
if baggage_sample_rand is not None:
807+
self._sample_rand = baggage_sample_rand
808+
else:
809+
self._sample_rand = Random(self.trace_id).random()
810+
802811
def __repr__(self):
803812
# type: () -> str
804813
return (
@@ -1167,10 +1176,10 @@ def _set_initial_sampling_decision(self, sampling_context):
11671176
self.sampled = False
11681177
return
11691178

1170-
# Now we roll the dice. random.random is inclusive of 0, but not of 1,
1179+
# Now we roll the dice. self._sample_rand is inclusive of 0, but not of 1,
11711180
# so strict < is safe here. In case sample_rate is a boolean, cast it
11721181
# to a float (True becomes 1.0 and False becomes 0.0)
1173-
self.sampled = random.random() < self.sample_rate
1182+
self.sampled = self._sample_rand < self.sample_rate
11741183

11751184
if self.sampled:
11761185
logger.debug(

sentry_sdk/tracing_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ def populate_from_transaction(cls, transaction):
630630
options = client.options or {}
631631

632632
sentry_items["trace_id"] = transaction.trace_id
633+
sentry_items["sample_rand"] = str(transaction._sample_rand)
633634

634635
if options.get("environment"):
635636
sentry_items["environment"] = options["environment"]
@@ -702,6 +703,20 @@ def strip_sentry_baggage(header):
702703
)
703704
)
704705

706+
def _sample_rand(self):
707+
# type: () -> Optional[float]
708+
"""Convenience method to get the sample_rand value from the sentry_items.
709+
710+
We validate the value and parse it as a float before returning it. The value is considered
711+
valid if it is a float in the range [0, 1).
712+
"""
713+
sample_rand = _try_float(self.sentry_items.get("sample_rand"))
714+
715+
if sample_rand is not None and 0 <= sample_rand < 1:
716+
return sample_rand
717+
718+
return None
719+
705720

706721
def should_propagate_trace(client, url):
707722
# type: (sentry_sdk.client.BaseClient, str) -> bool

tests/tracing/test_sample_rand.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import pytest
2+
3+
import sentry_sdk
4+
from sentry_sdk.tracing_utils import Baggage
5+
6+
TEST_TRACE_ID_SAMPLE_RANDS = {
7+
"00000000000000000000000000000000": 0.8766381713144122,
8+
"01234567012345670123456701234567": 0.6451742521664413,
9+
"0123456789abcdef0123456789abcdef": 0.9338861957669223,
10+
}
11+
"""
12+
A dictionary of some trace IDs used in the tests, and their precomputed sample_rand values.
13+
14+
sample_rand values are pseudo-random numbers, deterministically generated from the trace ID.
15+
"""
16+
17+
18+
@pytest.mark.parametrize(
19+
("trace_id", "expected_sample_rand"),
20+
TEST_TRACE_ID_SAMPLE_RANDS.items(),
21+
)
22+
# test 21 linearly spaced sample_rate values from 0.0 to 1.0, inclusive
23+
@pytest.mark.parametrize("sample_rate", (i / 20 for i in range(21)))
24+
def test_deterministic_sampled(
25+
sentry_init, capture_events, sample_rate, trace_id, expected_sample_rand
26+
):
27+
"""
28+
Test that the sample_rand value is deterministic based on the trace ID, and
29+
that it is used to determine the sampling decision. Also, ensure that the
30+
transaction's baggage contains the sample_rand value.
31+
"""
32+
sentry_init(traces_sample_rate=sample_rate)
33+
events = capture_events()
34+
35+
with sentry_sdk.start_transaction(trace_id=trace_id) as transaction:
36+
assert transaction.get_baggage().sentry_items["sample_rand"] == str(
37+
expected_sample_rand
38+
)
39+
40+
# Transaction event captured if sample_rand < sample_rate, indicating that
41+
# sample_rand is used to make the sampling decision.
42+
assert len(events) == int(expected_sample_rand < sample_rate)
43+
44+
45+
@pytest.mark.parametrize("sample_rand", (0.0, 0.2, 0.4, 0.6, 0.8))
46+
@pytest.mark.parametrize("sample_rate", (0.0, 0.2, 0.4, 0.6, 0.8, 1.0))
47+
def test_transaction_uses_incoming_sample_rand(
48+
sentry_init, capture_events, sample_rate, sample_rand
49+
):
50+
"""
51+
Test that the transaction uses the sample_rand value from the incoming baggage.
52+
"""
53+
baggage = Baggage(sentry_items={"sample_rand": str(sample_rand)})
54+
55+
sentry_init(traces_sample_rate=sample_rate)
56+
events = capture_events()
57+
58+
with sentry_sdk.start_transaction(baggage=baggage) as transaction:
59+
assert transaction.get_baggage().sentry_items["sample_rand"] == str(sample_rand)
60+
61+
# Transaction event captured if sample_rand < sample_rate, indicating that
62+
# sample_rand is used to make the sampling decision.
63+
assert len(events) == int(sample_rand < sample_rate)

0 commit comments

Comments
 (0)