|
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 |
|
@@ -418,13 +419,17 @@ def from_incoming_data(cls, incoming_data):
|
418 | 419 | propagation_context = PropagationContext()
|
419 | 420 | propagation_context.update(sentrytrace_data)
|
420 | 421 |
|
| 422 | + if propagation_context is not None: |
| 423 | + propagation_context._fill_sample_rand() |
| 424 | + |
421 | 425 | return propagation_context
|
422 | 426 |
|
423 | 427 | @property
|
424 | 428 | def trace_id(self):
|
425 | 429 | # type: () -> str
|
426 | 430 | """The trace id of the Sentry trace."""
|
427 | 431 | if not self._trace_id:
|
| 432 | + # New trace, don't fill in sample_rand |
428 | 433 | self._trace_id = uuid.uuid4().hex
|
429 | 434 |
|
430 | 435 | return self._trace_id
|
@@ -469,6 +474,48 @@ def __repr__(self):
|
469 | 474 | self.dynamic_sampling_context,
|
470 | 475 | )
|
471 | 476 |
|
| 477 | + def _fill_sample_rand(self): |
| 478 | + # type: () -> None |
| 479 | + """ |
| 480 | + Ensure that there is a valid sample_rand value in the dynamic_sampling_context. |
| 481 | +
|
| 482 | + If there is a valid sample_rand value in the dynamic_sampling_context, we keep it. |
| 483 | + Otherwise, we generate a sample_rand value according to the following: |
| 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: |
| 487 | + - [0, sample_rate) if parent_sampled is True, |
| 488 | + - or, in the range [sample_rate, 1) if parent_sampled is False. |
| 489 | +
|
| 490 | + - If either parent_sampled or sample_rate is missing, we generate a random |
| 491 | + value in the range [0, 1). |
| 492 | +
|
| 493 | + The sample_rand is deterministically generated from the trace_id, if present. |
| 494 | + """ |
| 495 | + # Ensure that the dynamic_sampling_context is a dict |
| 496 | + self.dynamic_sampling_context = self.dynamic_sampling_context or {} |
| 497 | + |
| 498 | + sample_rand = _try_float(self.dynamic_sampling_context.get("sample_rand")) |
| 499 | + if sample_rand is not None and 0 <= sample_rand < 1: |
| 500 | + # sample_rand is present and valid, so don't overwrite it |
| 501 | + return |
| 502 | + |
| 503 | + # Get the sample rate and compute the transformation that will map the random value |
| 504 | + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). |
| 505 | + sample_rate = _try_float(self.dynamic_sampling_context.get("sample_rate")) |
| 506 | + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) |
| 507 | + |
| 508 | + try: |
| 509 | + self.dynamic_sampling_context["sample_rand"] = str( |
| 510 | + _GuaranteedRangeRandom(self.trace_id).uniform(lower, upper) |
| 511 | + ) |
| 512 | + except ValueError: |
| 513 | + # lower >= upper in this case, indicating that the incoming trace had |
| 514 | + # a sample_rate that is inconsistent with the sampling decision (e.g. |
| 515 | + # sample_rate=0.0 but sampled=True). We cannot backfill a sensible |
| 516 | + # sample_rand value in this case. |
| 517 | + pass |
| 518 | + |
472 | 519 |
|
473 | 520 | class Baggage:
|
474 | 521 | """
|
@@ -748,6 +795,59 @@ def get_current_span(scope=None):
|
748 | 795 | return current_span
|
749 | 796 |
|
750 | 797 |
|
| 798 | +def _try_float(value): |
| 799 | + # type: (Any) -> Optional[float] |
| 800 | + """Small utility to convert a value to a float, if possible.""" |
| 801 | + try: |
| 802 | + return float(value) |
| 803 | + except (ValueError, TypeError): |
| 804 | + return None |
| 805 | + |
| 806 | + |
| 807 | +def _sample_rand_range(parent_sampled, sample_rate): |
| 808 | + # type: (Optional[bool], Optional[float]) -> tuple[float, float] |
| 809 | + """ |
| 810 | + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values |
| 811 | + that a generated sample_rand value must fall into, given the parent_sampled and |
| 812 | + sample_rate values. |
| 813 | + """ |
| 814 | + if parent_sampled is None or sample_rate is None: |
| 815 | + return 0.0, 1.0 |
| 816 | + elif parent_sampled is True: |
| 817 | + return 0.0, sample_rate |
| 818 | + else: # parent_sampled is False |
| 819 | + return sample_rate, 1.0 |
| 820 | + |
| 821 | + |
| 822 | +class _GuaranteedRangeRandom: |
| 823 | + """ |
| 824 | + A random number generator with a uniform implementation that guarantees |
| 825 | + a return value in lower <= x < upper. |
| 826 | + """ |
| 827 | + |
| 828 | + def __init__(self, seed): |
| 829 | + # type: (Optional[str]) -> None |
| 830 | + self._random = Random(seed) |
| 831 | + |
| 832 | + def uniform(self, lower, upper): |
| 833 | + # type: (float, float) -> float |
| 834 | + """ |
| 835 | + Return a random number in the range lower <= x < upper. |
| 836 | + Raises an error if lower >= upper, as it would be impossible to generate |
| 837 | + a value in the range lower <= x < upper in such a case. |
| 838 | + """ |
| 839 | + if lower >= upper: |
| 840 | + raise ValueError("lower must be strictly less than upper") |
| 841 | + |
| 842 | + rv = upper |
| 843 | + while rv == upper: |
| 844 | + # The built-in uniform() method can, in some cases, return the |
| 845 | + # upper bound. We request a new value until we get a different |
| 846 | + # value. |
| 847 | + rv = self._random.uniform(lower, upper) |
| 848 | + return rv |
| 849 | + |
| 850 | + |
751 | 851 | # Circular imports
|
752 | 852 | from sentry_sdk.tracing import (
|
753 | 853 | BAGGAGE_HEADER_NAME,
|
|
0 commit comments