diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index c9b357305a..446a191335 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,4 +1,3 @@ -from decimal import Decimal import uuid import warnings from datetime import datetime, timedelta, timezone @@ -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( diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index b31d3d85c5..c1cfde293b 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -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 @@ -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 @@ -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"] @@ -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 @@ -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 @@ -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): diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index b0f3fa4a4c..3fe3ac3eec 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -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: diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index dbb4286370..267ce08fdd 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -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", diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index ce2e693143..80b4a423cb 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -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), diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index 5a35b68076..ba2575ce59 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -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", diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index f6735d0e74..b8d46d0558 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -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: diff --git a/tests/test_dsc.py b/tests/test_dsc.py index 8e549d0cf8..6097af7f95 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -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 diff --git a/tests/test_monitor.py b/tests/test_monitor.py index b48d9f6282..9ffc943bed 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -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 diff --git a/tests/test_propagationcontext.py b/tests/test_propagationcontext.py index a0ce1094fa..078a69c72b 100644 --- a/tests/test_propagationcontext.py +++ b/tests/test_propagationcontext.py @@ -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): @@ -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): diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 61ef14b7d0..8b5659b694 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -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 diff --git a/tests/tracing/test_sample_rand.py b/tests/tracing/test_sample_rand.py index f9c10aa04e..4a74950b30 100644 --- a/tests/tracing/test_sample_rand.py +++ b/tests/tracing/test_sample_rand.py @@ -1,5 +1,3 @@ -import decimal -from decimal import Inexact, FloatOperation from unittest import mock import pytest @@ -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 ( @@ -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 diff --git a/tests/tracing/test_sample_rand_propagation.py b/tests/tracing/test_sample_rand_propagation.py index ea3ea548ff..e6f3e99510 100644 --- a/tests/tracing/test_sample_rand_propagation.py +++ b/tests/tracing/test_sample_rand_propagation.py @@ -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"