6
6
from collections .abc import Mapping
7
7
from datetime import timedelta
8
8
from functools import wraps
9
+ from random import Random
9
10
from urllib .parse import quote , unquote
10
11
import uuid
11
12
@@ -397,6 +398,8 @@ def __init__(
397
398
self .dynamic_sampling_context = dynamic_sampling_context
398
399
"""Data that is used for dynamic sampling decisions."""
399
400
401
+ self ._fill_sample_rand ()
402
+
400
403
@classmethod
401
404
def from_incoming_data (cls , incoming_data ):
402
405
# type: (Dict[str, Any]) -> Optional[PropagationContext]
@@ -418,6 +421,9 @@ def from_incoming_data(cls, incoming_data):
418
421
propagation_context = PropagationContext ()
419
422
propagation_context .update (sentrytrace_data )
420
423
424
+ if propagation_context is not None :
425
+ propagation_context ._fill_sample_rand ()
426
+
421
427
return propagation_context
422
428
423
429
@property
@@ -426,6 +432,7 @@ def trace_id(self):
426
432
"""The trace id of the Sentry trace."""
427
433
if not self ._trace_id :
428
434
self ._trace_id = uuid .uuid4 ().hex
435
+ self ._fill_sample_rand ()
429
436
430
437
return self ._trace_id
431
438
@@ -469,6 +476,45 @@ def __repr__(self):
469
476
self .dynamic_sampling_context ,
470
477
)
471
478
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
+
472
518
473
519
class Baggage :
474
520
"""
@@ -744,6 +790,35 @@ def get_current_span(scope=None):
744
790
return current_span
745
791
746
792
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
+
747
822
# Circular imports
748
823
from sentry_sdk .tracing import (
749
824
BAGGAGE_HEADER_NAME ,
0 commit comments