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,8 @@ def from_incoming_data(cls, incoming_data):
418421 propagation_context = PropagationContext ()
419422 propagation_context .update (sentrytrace_data )
420423
424+ propagation_context ._fill_sample_rand ()
425+
421426 return propagation_context
422427
423428 @property
@@ -426,6 +431,7 @@ def trace_id(self):
426431 """The trace id of the Sentry trace."""
427432 if not self ._trace_id :
428433 self ._trace_id = uuid .uuid4 ().hex
434+ self ._fill_sample_rand ()
429435
430436 return self ._trace_id
431437
@@ -469,6 +475,45 @@ def __repr__(self):
469475 self .dynamic_sampling_context ,
470476 )
471477
478+ def _fill_sample_rand (self ):
479+ """
480+ If the sample_rand is missing from the Dynamic Sampling Context (or invalid),
481+ we generate it here.
482+
483+ We only generate a sample_rand if the trace_id is set.
484+
485+ If we have a parent_sampled value and a sample_rate in the DSC, we compute
486+ a sample_rand value randomly in the range [0, sample_rate) if parent_sampled is True,
487+ or in the range [sample_rate, 1) if parent_sampled is False. If either parent_sampled
488+ or sample_rate is missing, we generate a random value in the range [0, 1).
489+
490+ The sample_rand is deterministically generated from the trace_id.
491+ """
492+ if self ._trace_id is None :
493+ # We only want to generate a sample_rand if the _trace_id is set.
494+ return
495+
496+ # Ensure that the dynamic_sampling_context is a dict
497+ self .dynamic_sampling_context = self .dynamic_sampling_context or {}
498+
499+ sample_rand = _try_float (self .dynamic_sampling_context .get ("sample_rand" ))
500+ if sample_rand is not None and 0 <= sample_rand < 1 :
501+ # sample_rand is present and valid, so don't overwrite it
502+ return
503+
504+ # Get a random value in [0, 1)
505+ random_value = Random (self .trace_id ).random ()
506+
507+ # Get the sample rate and compute the transformation that will map the random value
508+ # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1).
509+ sample_rate = _try_float (self .dynamic_sampling_context .get ("sample_rate" ))
510+ factor , offset = _sample_rand_transormation (self .parent_sampled , sample_rate )
511+
512+ # Transform the random value to the desired range
513+ self .dynamic_sampling_context ["sample_rand" ] = str (
514+ random_value * factor + offset
515+ )
516+
472517
473518class Baggage :
474519 """
@@ -744,6 +789,35 @@ def get_current_span(scope=None):
744789 return current_span
745790
746791
792+ def _try_float (value ):
793+ # type: (object) -> Optional[float]
794+ """Small utility to convert a value to a float, if possible."""
795+ try :
796+ return float (value )
797+ except (ValueError , TypeError ):
798+ return None
799+
800+
801+ def _sample_rand_transormation (parent_sampled , sample_rate ):
802+ # type: (Optional[bool], Optional[float]) -> tuple[float, float]
803+ """
804+ Compute the factor and offset to scale and translate a random number in [0, 1) to
805+ a range consistent with the parent_sampled and sample_rate values.
806+
807+ The return value is a tuple (factor, offset) such that, given random_value in [0, 1),
808+ and new_value = random_value * factor + offset:
809+ - new_value will be unchanged if either parent_sampled or sample_rate is None
810+ - if parent_sampled and sample_rate are both set, new_value will be in [0, sample_rate)
811+ if parent_sampled is True, or in [sample_rate, 1) if parent_sampled is False
812+ """
813+ if parent_sampled is None or sample_rate is None :
814+ return 1.0 , 0.0
815+ elif parent_sampled is True :
816+ return sample_rate , 0.0
817+ else : # parent_sampled is False
818+ return 1.0 - sample_rate , sample_rate
819+
820+
747821# Circular imports
748822from sentry_sdk .tracing import (
749823 BAGGAGE_HEADER_NAME ,
0 commit comments