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 17 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
39 changes: 13 additions & 26 deletions sentry_sdk/integrations/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@
import asyncio
import inspect
from copy import deepcopy
from functools import partial
from functools import lru_cache, partial

import sentry_sdk
from sentry_sdk.api import continue_trace
from sentry_sdk.consts import OP

from sentry_sdk.integrations._asgi_common import (
_get_headers,
_get_request_data,
Expand Down Expand Up @@ -42,7 +41,6 @@

if TYPE_CHECKING:
from typing import Any
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Tuple
Expand All @@ -68,6 +66,7 @@ def _capture_exception(exc, mechanism_type="asgi"):
sentry_sdk.capture_event(event, hint=hint)


@lru_cache(maxsize=5)
def _looks_like_asgi3(app):
# type: (Any) -> bool
"""
Expand Down Expand Up @@ -102,6 +101,7 @@ def __init__(
mechanism_type="asgi", # type: str
span_origin="manual", # type: str
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
asgi_version=None, # type: Optional[int]
):
# type: (...) -> None
"""
Expand Down Expand Up @@ -140,10 +140,16 @@ def __init__(
self.app = app
self.http_methods_to_capture = http_methods_to_capture

if _looks_like_asgi3(app):
self.__call__ = self._run_asgi3 # type: Callable[..., Any]
else:
self.__call__ = self._run_asgi2
if asgi_version is None:
if _looks_like_asgi3(app):
asgi_version = 3
else:
asgi_version = 2

if asgi_version == 3:
self.__call__ = self._run_asgi3
elif asgi_version == 2:
self.__call__ = self._run_asgi2 # type: ignore

def _capture_lifespan_exception(self, exc):
# type: (Exception) -> None
Expand Down Expand Up @@ -217,28 +223,16 @@ async def _run_app(self, scope, receive, send, asgi_version):
source=transaction_source,
origin=self.span_origin,
)
logger.debug(
"[ASGI] Created transaction (continuing trace): %s",
transaction,
)
else:
transaction = Transaction(
op=OP.HTTP_SERVER,
name=transaction_name,
source=transaction_source,
origin=self.span_origin,
)
logger.debug(
"[ASGI] Created transaction (new): %s", transaction
)

if transaction:
transaction.set_tag("asgi.type", ty)
logger.debug(
"[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
transaction.name,
transaction.source,
)

with (
sentry_sdk.start_transaction(
Expand All @@ -248,7 +242,6 @@ async def _run_app(self, scope, receive, send, asgi_version):
if transaction is not None
else nullcontext()
):
logger.debug("[ASGI] Started transaction: %s", transaction)
try:

async def _sentry_wrapped_send(event):
Expand Down Expand Up @@ -303,12 +296,6 @@ def event_processor(self, event, hint, asgi_scope):
event["transaction"] = name
event["transaction_info"] = {"source": source}

logger.debug(
"[ASGI] Set transaction name and source in event_processor: '%s' / '%s'",
event["transaction"],
event["transaction_info"]["source"],
)

return event

# Helper functions.
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/django/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ async def sentry_patched_asgi_handler(self, receive, send):
http_methods_to_capture=integration.http_methods_to_capture,
)

return await middleware(self.scope)(receive, send)
return await middleware(self.scope)(receive, send) # type: ignore

cls.__call__ = sentry_patched_asgi_handler

Expand Down
8 changes: 1 addition & 7 deletions sentry_sdk/integrations/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
from sentry_sdk.integrations import DidNotEnable
from sentry_sdk.scope import should_send_default_pii
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
from sentry_sdk.utils import (
transaction_from_function,
logger,
)
from sentry_sdk.utils import transaction_from_function

from typing import TYPE_CHECKING

Expand Down Expand Up @@ -66,9 +63,6 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
source = SOURCE_FOR_STYLE[transaction_style]

scope.set_transaction_name(name, source=source)
logger.debug(
"[FastAPI] Set transaction name and source on scope: %s / %s", name, source
)


def patch_get_request_handler():
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/litestar.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def __init__(self, app, span_origin=LitestarIntegration.origin):
transaction_style="endpoint",
mechanism_type="asgi",
span_origin=span_origin,
asgi_version=3,
)

def _capture_request_exception(self, exc):
Expand Down Expand Up @@ -116,7 +117,6 @@ def injection_wrapper(self, *args, **kwargs):
*(kwargs.get("after_exception") or []),
]

SentryLitestarASGIMiddleware.__call__ = SentryLitestarASGIMiddleware._run_asgi3 # type: ignore
middleware = kwargs.get("middleware") or []
kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware]
old__init__(self, *args, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/quart.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ async def sentry_patched_asgi_app(self, scope, receive, send):
middleware = SentryAsgiMiddleware(
lambda *a, **kw: old_app(self, *a, **kw),
span_origin=QuartIntegration.origin,
asgi_version=3,
)
middleware.__call__ = middleware._run_asgi3
return await middleware(scope, receive, send)

Quart.__call__ = sentry_patched_asgi_app
Expand Down
6 changes: 1 addition & 5 deletions sentry_sdk/integrations/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
capture_internal_exceptions,
ensure_integration_enabled,
event_from_exception,
logger,
parse_version,
transaction_from_function,
)
Expand Down Expand Up @@ -403,9 +402,9 @@ async def _sentry_patched_asgi_app(self, scope, receive, send):
if integration
else DEFAULT_HTTP_METHODS_TO_CAPTURE
),
asgi_version=3,
)

middleware.__call__ = middleware._run_asgi3
return await middleware(scope, receive, send)

Starlette.__call__ = _sentry_patched_asgi_app
Expand Down Expand Up @@ -723,9 +722,6 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
source = TransactionSource.ROUTE

scope.set_transaction_name(name, source=source)
logger.debug(
"[Starlette] Set transaction name and source on scope: %s / %s", name, source
)


def _get_transaction_from_middleware(app, asgi_scope, integration):
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/starlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(self, app, span_origin=StarliteIntegration.origin):
transaction_style="endpoint",
mechanism_type="asgi",
span_origin=span_origin,
asgi_version=3,
)


Expand Down Expand Up @@ -94,7 +95,6 @@ def injection_wrapper(self, *args, **kwargs):
]
)

SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3 # type: ignore
middleware = kwargs.get("middleware") or []
kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware]
old__init__(self, *args, **kwargs)
Expand Down
4 changes: 1 addition & 3 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 @@ -822,7 +821,6 @@ def __init__( # type: ignore[misc]
**kwargs, # type: Unpack[SpanKwargs]
):
# type: (...) -> None

super().__init__(**kwargs)

self.name = name
Expand Down Expand Up @@ -1224,7 +1222,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
33 changes: 11 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 @@ -501,7 +500,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 @@ -649,7 +648,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 @@ -723,15 +722,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 @@ -867,7 +866,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 @@ -882,19 +881,9 @@ 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,
)
sample_rand_scaled = rng.randrange(int(lower * 1000000), int(upper * 1000000))

return sample_rand_scaled / 1000000


def _sample_rand_range(parent_sampled, sample_rate):
Expand Down
6 changes: 5 additions & 1 deletion sentry_sdk/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from collections import namedtuple
from datetime import datetime, timezone
from decimal import Decimal
from functools import partial, partialmethod, wraps
from functools import lru_cache, partial, partialmethod, wraps
from numbers import Real
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit

Expand Down Expand Up @@ -1858,6 +1858,7 @@ def is_module_patched(mod_name):
return False


@lru_cache(maxsize=1)
def is_gevent():
# type: () -> bool
return is_module_patched("threading") or is_module_patched("_thread")
Expand Down Expand Up @@ -1934,6 +1935,9 @@ def try_convert(convert_func, value):
given function. Return None if the conversion fails, i.e. if the function
raises an exception.
"""
if isinstance(value, convert_func): # type: ignore
return value

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
Loading