Skip to content

ref(tracing): Use float for sample rand #4677

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a163f83
Do not set and then overwrite middlewares __call__
sentrivana Aug 5, 2025
6aec4fb
Remove logging from asgi
sentrivana Aug 6, 2025
a454b21
Merge branch 'master' into ivana/random-perf-improvements
sentrivana Aug 6, 2025
8278cf3
format
sentrivana Aug 6, 2025
f63d40c
Cache is_gevent
sentrivana Aug 6, 2025
78368b3
Cache _looks_like_asgi3
sentrivana Aug 6, 2025
155e9aa
Do not guess ASGI version in Quart integration
sentrivana Aug 6, 2025
030423c
Remove Decimal
sentrivana Aug 6, 2025
4516e8e
.
sentrivana Aug 6, 2025
f81c8f4
adapt tests
sentrivana Aug 6, 2025
0e37a51
Use asgi_version in Starlite, Litestar too
sentrivana Aug 7, 2025
9f0c233
mypy fixes
sentrivana Aug 7, 2025
e619405
Merge branch 'ivana/random-perf-improvements' into ivana/remove-decimal
sentrivana Aug 7, 2025
566eca3
mypy
sentrivana Aug 7, 2025
2866369
Merge branch 'master' into ivana/random-perf-improvements
sentrivana Aug 7, 2025
dff793d
Merge branch 'ivana/random-perf-improvements' into ivana/remove-decimal
sentrivana Aug 7, 2025
3836e98
Merge branch 'master' into ivana/remove-decimal
sentrivana Aug 12, 2025
e46a07e
Merge branch 'master' into ivana/remove-decimal
sentrivana Aug 13, 2025
a986f61
.
sentrivana Aug 14, 2025
c019d6d
Merge branch 'master' into ivana/remove-decimal
sentrivana Aug 14, 2025
c43a3b4
remove unrelated change from old base branch
sentrivana Aug 14, 2025
10128c2
facepalm
sentrivana Aug 14, 2025
2f72376
more unused stuff from old base branch;
sentrivana Aug 14, 2025
7c10d00
.
sentrivana Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from decimal import Decimal
import uuid
import warnings
from datetime import datetime, timedelta, timezone
Expand Down Expand Up @@ -1249,7 +1248,7 @@ def _set_initial_sampling_decision(self, sampling_context):
return

# Now we roll the dice.
self.sampled = self._sample_rand < Decimal.from_float(self.sample_rate)
self.sampled = self._sample_rand < self.sample_rate

if self.sampled:
logger.debug(
Expand Down
40 changes: 18 additions & 22 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import sys
from collections.abc import Mapping
from datetime import timedelta
from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext
from random import Random
from urllib.parse import quote, unquote
import uuid
Expand Down Expand Up @@ -502,7 +501,7 @@ def _fill_sample_rand(self):
return

sample_rand = try_convert(
Decimal, self.dynamic_sampling_context.get("sample_rand")
float, self.dynamic_sampling_context.get("sample_rand")
)
if sample_rand is not None and 0 <= sample_rand < 1:
# sample_rand is present and valid, so don't overwrite it
Expand Down Expand Up @@ -650,7 +649,7 @@ def populate_from_transaction(cls, transaction):
options = client.options or {}

sentry_items["trace_id"] = transaction.trace_id
sentry_items["sample_rand"] = str(transaction._sample_rand)
sentry_items["sample_rand"] = f"{transaction._sample_rand:.6f}" # noqa: E231

if options.get("environment"):
sentry_items["environment"] = options["environment"]
Expand Down Expand Up @@ -724,15 +723,15 @@ def strip_sentry_baggage(header):
)

def _sample_rand(self):
# type: () -> Optional[Decimal]
# type: () -> Optional[float]
"""Convenience method to get the sample_rand value from the sentry_items.

We validate the value and parse it as a Decimal before returning it. The value is considered
valid if it is a Decimal in the range [0, 1).
We validate the value and parse it as a float before returning it. The value is considered
valid if it is a float in the range [0, 1).
"""
sample_rand = try_convert(Decimal, self.sentry_items.get("sample_rand"))
sample_rand = try_convert(float, self.sentry_items.get("sample_rand"))

if sample_rand is not None and Decimal(0) <= sample_rand < Decimal(1):
if sample_rand is not None and 0.0 <= sample_rand < 1.0:
return sample_rand

return None
Expand Down Expand Up @@ -898,7 +897,7 @@ def _generate_sample_rand(
*,
interval=(0.0, 1.0), # type: tuple[float, float]
):
# type: (...) -> Decimal
# type: (...) -> float
"""Generate a sample_rand value from a trace ID.

The generated value will be pseudorandomly chosen from the provided
Expand All @@ -913,19 +912,16 @@ def _generate_sample_rand(
raise ValueError("Invalid interval: lower must be less than upper")

rng = Random(trace_id)
sample_rand = upper
while sample_rand >= upper:
sample_rand = rng.uniform(lower, upper)

# Round down to exactly six decimal-digit precision.
# Setting the context is needed to avoid an InvalidOperation exception
# in case the user has changed the default precision or set traps.
with localcontext(DefaultContext) as ctx:
ctx.prec = 6
return Decimal(sample_rand).quantize(
Decimal("0.000001"),
rounding=ROUND_DOWN,
)
lower_scaled = int(lower * 1_000_000)
upper_scaled = int(upper * 1_000_000)
try:
sample_rand_scaled = rng.randrange(lower_scaled, upper_scaled)
except ValueError:
# In some corner cases it might happen that the range is too small
# In that case, just take the lower bound
sample_rand_scaled = lower_scaled

return sample_rand_scaled / 1_000_000


def _sample_rand_range(parent_sampled, sample_rate):
Expand Down
6 changes: 6 additions & 0 deletions sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,12 @@ def try_convert(convert_func, value):
given function. Return None if the conversion fails, i.e. if the function
raises an exception.
"""
try:
if isinstance(value, convert_func): # type: ignore
return value
except TypeError:
pass

try:
return convert_func(value)
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion tests/integrations/aiohttp/test_aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@ async def handler(request):

raw_server = await aiohttp_raw_server(handler)

with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/celery/test_celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,8 +518,8 @@ def test_baggage_propagation(init_celery):
def dummy_task(self, x, y):
return _get_headers(self)

# patch random.uniform to return a predictable sample_rand value
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
# patch random.randrange to return a predictable sample_rand value
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
with start_transaction() as transaction:
result = dummy_task.apply_async(
args=(1, 0),
Expand Down
4 changes: 2 additions & 2 deletions tests/integrations/httpx/test_httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ def test_outgoing_trace_headers_append_to_baggage(

url = "http://example.com/"

# patch random.uniform to return a predictable sample_rand value
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
# patch random.randrange to return a predictable sample_rand value
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000):
with start_transaction(
name="/interactions/other-dogs/new-dog",
op="greeting.sniff",
Expand Down
2 changes: 1 addition & 1 deletion tests/integrations/stdlib/test_httplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
monkeypatch.setattr(HTTPSConnection, "send", mock_send)

sentry_init(traces_sample_rate=0.5, release="foo")
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25):
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000):
transaction = Transaction.continue_from_headers({})

with start_transaction(transaction=transaction, name="Head SDK tx") as transaction:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dsc.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def my_traces_sampler(sampling_context):
}

# We continue the incoming trace and start a new transaction
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.125):
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=125000):
transaction = sentry_sdk.continue_trace(incoming_http_headers)
with sentry_sdk.start_transaction(transaction, name="foo"):
pass
Expand Down
2 changes: 1 addition & 1 deletion tests/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_transaction_uses_downsampled_rate(
assert monitor.downsample_factor == 1

# make sure we don't sample the transaction
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.75):
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=750000):
with sentry_sdk.start_transaction(name="foobar") as transaction:
assert transaction.sampled is False
assert transaction.sample_rate == 0.5
Expand Down
17 changes: 10 additions & 7 deletions tests/test_propagationcontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,13 @@ def test_sample_rand_filled(parent_sampled, sample_rate, expected_interval):
else:
sample_rate_str = ""

# for convenience, we'll just return the lower bound of the interval
mock_uniform = mock.Mock(return_value=expected_interval[0])
# for convenience, we'll just return the lower bound of the interval as an integer
mock_randrange = mock.Mock(return_value=int(expected_interval[0] * 1000000))

def mock_random_class(seed):
assert seed == "00000000000000000000000000000000", "seed should be the trace_id"
rv = Mock()
rv.uniform = mock_uniform
rv.randrange = mock_randrange
return rv

with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class):
Expand All @@ -158,17 +158,20 @@ def mock_random_class(seed):
ctx.dynamic_sampling_context["sample_rand"]
== f"{expected_interval[0]:.6f}" # noqa: E231
)
assert mock_uniform.call_count == 1
assert mock_uniform.call_args[0] == expected_interval
assert mock_randrange.call_count == 1
assert mock_randrange.call_args[0] == (
int(expected_interval[0] * 1000000),
int(expected_interval[1] * 1000000),
)


def test_sample_rand_rounds_down():
# Mock value that should round down to 0.999_999
mock_uniform = mock.Mock(return_value=0.999_999_9)
mock_randrange = mock.Mock(return_value=999999)

def mock_random_class(_):
rv = Mock()
rv.uniform = mock_uniform
rv.randrange = mock_randrange
return rv

with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class):
Expand Down
2 changes: 1 addition & 1 deletion tests/tracing/test_integration_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ def test_dynamic_sampling_head_sdk_creates_dsc(
envelopes = capture_envelopes()

# make sure transaction is sampled for both cases
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25):
with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=250000):
transaction = Transaction.continue_from_headers({}, name="Head SDK tx")

# will create empty mutable baggage
Expand Down
37 changes: 2 additions & 35 deletions tests/tracing/test_sample_rand.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import decimal
from decimal import Inexact, FloatOperation
from unittest import mock

import pytest
Expand All @@ -20,7 +18,8 @@ def test_deterministic_sampled(sentry_init, capture_events, sample_rate, sample_
events = capture_events()

with mock.patch(
"sentry_sdk.tracing_utils.Random.uniform", return_value=sample_rand
"sentry_sdk.tracing_utils.Random.randrange",
return_value=int(sample_rand * 1000000),
):
with sentry_sdk.start_transaction() as transaction:
assert (
Expand Down Expand Up @@ -55,35 +54,3 @@ def test_transaction_uses_incoming_sample_rand(
# Transaction event captured if sample_rand < sample_rate, indicating that
# sample_rand is used to make the sampling decision.
assert len(events) == int(sample_rand < sample_rate)


def test_decimal_context(sentry_init, capture_events):
"""
Ensure that having a user altered decimal context with a precision below 6
does not cause an InvalidOperation exception.
"""
sentry_init(traces_sample_rate=1.0)
events = capture_events()

old_prec = decimal.getcontext().prec
old_inexact = decimal.getcontext().traps[Inexact]
old_float_operation = decimal.getcontext().traps[FloatOperation]

decimal.getcontext().prec = 2
decimal.getcontext().traps[Inexact] = True
decimal.getcontext().traps[FloatOperation] = True

try:
with mock.patch(
"sentry_sdk.tracing_utils.Random.uniform", return_value=0.123456789
):
with sentry_sdk.start_transaction() as transaction:
assert (
transaction.get_baggage().sentry_items["sample_rand"] == "0.123456"
)
finally:
decimal.getcontext().prec = old_prec
decimal.getcontext().traps[Inexact] = old_inexact
decimal.getcontext().traps[FloatOperation] = old_float_operation

assert len(events) == 1
6 changes: 3 additions & 3 deletions tests/tracing/test_sample_rand_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ def test_continue_trace_missing_sample_rand():
"baggage": "sentry-placeholder=asdf",
}

mock_uniform = Mock(return_value=0.5)

with mock.patch("sentry_sdk.tracing_utils.Random.uniform", mock_uniform):
with mock.patch(
"sentry_sdk.tracing_utils.Random.randrange", Mock(return_value=500000)
):
transaction = sentry_sdk.continue_trace(headers)

assert transaction.get_baggage().sentry_items["sample_rand"] == "0.500000"