66from collections .abc import Mapping
77from datetime import timedelta
88from functools import wraps
9+ from random import Random
910from urllib .parse import quote , unquote
1011import uuid
1112
@@ -397,6 +398,8 @@ def __init__(
397398 self .dynamic_sampling_context = dynamic_sampling_context
398399 """Data that is used for dynamic sampling decisions."""
399400
401+ self ._fill_sample_rand ()
402+
400403 @classmethod
401404 def from_incoming_data (cls , incoming_data ):
402405 # type: (Dict[str, Any]) -> Optional[PropagationContext]
@@ -418,6 +421,9 @@ def from_incoming_data(cls, incoming_data):
418421 propagation_context = PropagationContext ()
419422 propagation_context .update (sentrytrace_data )
420423
424+ if propagation_context is not None :
425+ propagation_context ._fill_sample_rand ()
426+
421427 return propagation_context
422428
423429 @property
@@ -426,6 +432,7 @@ def trace_id(self):
426432 """The trace id of the Sentry trace."""
427433 if not self ._trace_id :
428434 self ._trace_id = uuid .uuid4 ().hex
435+ self ._fill_sample_rand ()
429436
430437 return self ._trace_id
431438
@@ -469,6 +476,45 @@ def __repr__(self):
469476 self .dynamic_sampling_context ,
470477 )
471478
479+ def _fill_sample_rand (self ):
480+ """
481+ If the sample_rand is missing from the Dynamic Sampling Context (or invalid),
482+ we generate it here.
483+
484+ We only generate a sample_rand if the trace_id is set.
485+
486+ If we have a parent_sampled value and a sample_rate in the DSC, we compute
487+ a sample_rand value randomly in the range [0, sample_rate) if parent_sampled is True,
488+ or in the range [sample_rate, 1) if parent_sampled is False. If either parent_sampled
489+ or sample_rate is missing, we generate a random value in the range [0, 1).
490+
491+ The sample_rand is deterministically generated from the trace_id.
492+ """
493+ if self ._trace_id is None :
494+ # We only want to generate a sample_rand if the _trace_id is set.
495+ return
496+
497+ # Ensure that the dynamic_sampling_context is a dict
498+ self .dynamic_sampling_context = self .dynamic_sampling_context or {}
499+
500+ sample_rand = _try_float (self .dynamic_sampling_context .get ("sample_rand" ))
501+ if sample_rand is not None and 0 <= sample_rand < 1 :
502+ # sample_rand is present and valid, so don't overwrite it
503+ return
504+
505+ # Get a random value in [0, 1)
506+ random_value = Random (self .trace_id ).random ()
507+
508+ # Get the sample rate and compute the transformation that will map the random value
509+ # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1).
510+ sample_rate = _try_float (self .dynamic_sampling_context .get ("sample_rate" ))
511+ factor , offset = _sample_rand_transormation (self .parent_sampled , sample_rate )
512+
513+ # Transform the random value to the desired range
514+ self .dynamic_sampling_context ["sample_rand" ] = str (
515+ random_value * factor + offset
516+ )
517+
472518
473519class Baggage :
474520 """
@@ -744,6 +790,35 @@ def get_current_span(scope=None):
744790 return current_span
745791
746792
793+ def _try_float (value ):
794+ # type: (object) -> Optional[float]
795+ """Small utility to convert a value to a float, if possible."""
796+ try :
797+ return float (value )
798+ except (ValueError , TypeError ):
799+ return None
800+
801+
802+ def _sample_rand_transormation (parent_sampled , sample_rate ):
803+ # type: (Optional[bool], Optional[float]) -> tuple[float, float]
804+ """
805+ Compute the factor and offset to scale and translate a random number in [0, 1) to
806+ a range consistent with the parent_sampled and sample_rate values.
807+
808+ The return value is a tuple (factor, offset) such that, given random_value in [0, 1),
809+ and new_value = random_value * factor + offset:
810+ - new_value will be unchanged if either parent_sampled or sample_rate is None
811+ - if parent_sampled and sample_rate are both set, new_value will be in [0, sample_rate)
812+ if parent_sampled is True, or in [sample_rate, 1) if parent_sampled is False
813+ """
814+ if parent_sampled is None or sample_rate is None :
815+ return 1.0 , 0.0
816+ elif parent_sampled is True :
817+ return sample_rate , 0.0
818+ else : # parent_sampled is False
819+ return 1.0 - sample_rate , sample_rate
820+
821+
747822# Circular imports
748823from sentry_sdk .tracing import (
749824 BAGGAGE_HEADER_NAME ,
0 commit comments