|
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 |
|
|
19 | 20 | match_regex_list,
|
20 | 21 | qualname_from_function,
|
21 | 22 | to_string,
|
| 23 | + try_float, |
22 | 24 | is_sentry_url,
|
23 | 25 | _is_external_source,
|
24 | 26 | _is_in_project_root,
|
@@ -418,13 +420,17 @@ def from_incoming_data(cls, incoming_data):
|
418 | 420 | propagation_context = PropagationContext()
|
419 | 421 | propagation_context.update(sentrytrace_data)
|
420 | 422 |
|
| 423 | + if propagation_context is not None: |
| 424 | + propagation_context._fill_sample_rand() |
| 425 | + |
421 | 426 | return propagation_context
|
422 | 427 |
|
423 | 428 | @property
|
424 | 429 | def trace_id(self):
|
425 | 430 | # type: () -> str
|
426 | 431 | """The trace id of the Sentry trace."""
|
427 | 432 | if not self._trace_id:
|
| 433 | + # New trace, don't fill in sample_rand |
428 | 434 | self._trace_id = uuid.uuid4().hex
|
429 | 435 |
|
430 | 436 | return self._trace_id
|
@@ -469,6 +475,55 @@ def __repr__(self):
|
469 | 475 | self.dynamic_sampling_context,
|
470 | 476 | )
|
471 | 477 |
|
| 478 | + def _fill_sample_rand(self): |
| 479 | + # type: () -> None |
| 480 | + """ |
| 481 | + Ensure that there is a valid sample_rand value in the dynamic_sampling_context. |
| 482 | +
|
| 483 | + If there is a valid sample_rand value in the dynamic_sampling_context, we keep it. |
| 484 | + Otherwise, we generate a sample_rand value according to the following: |
| 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: |
| 488 | + - [0, sample_rate) if parent_sampled is True, |
| 489 | + - or, in the range [sample_rate, 1) if parent_sampled is False. |
| 490 | +
|
| 491 | + - If either parent_sampled or sample_rate is missing, we generate a random |
| 492 | + value in the range [0, 1). |
| 493 | +
|
| 494 | + The sample_rand is deterministically generated from the trace_id, if present. |
| 495 | +
|
| 496 | + This function does nothing if there is no dynamic_sampling_context. |
| 497 | + """ |
| 498 | + if self.dynamic_sampling_context is None: |
| 499 | + return |
| 500 | + |
| 501 | + sample_rand = try_float(self.dynamic_sampling_context.get("sample_rand")) |
| 502 | + if sample_rand is not None and 0 <= sample_rand < 1: |
| 503 | + # sample_rand is present and valid, so don't overwrite it |
| 504 | + return |
| 505 | + |
| 506 | + # Get the sample rate and compute the transformation that will map the random value |
| 507 | + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). |
| 508 | + sample_rate = try_float(self.dynamic_sampling_context.get("sample_rate")) |
| 509 | + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) |
| 510 | + |
| 511 | + if lower >= upper: |
| 512 | + # lower >= upper might happen if the incoming trace's sampled flag |
| 513 | + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. |
| 514 | + # We cannot generate a sensible sample_rand value in this case. |
| 515 | + return |
| 516 | + |
| 517 | + random = Random(self.trace_id) |
| 518 | + sample_rand = upper |
| 519 | + while sample_rand == upper: |
| 520 | + # The built-in uniform() method can, in some cases, return the |
| 521 | + # upper bound. We request a new value until we get a different |
| 522 | + # value. |
| 523 | + sample_rand = random.uniform(lower, upper) |
| 524 | + |
| 525 | + self.dynamic_sampling_context["sample_rand"] = str(sample_rand) |
| 526 | + |
472 | 527 |
|
473 | 528 | class Baggage:
|
474 | 529 | """
|
@@ -748,6 +803,21 @@ def get_current_span(scope=None):
|
748 | 803 | return current_span
|
749 | 804 |
|
750 | 805 |
|
| 806 | +def _sample_rand_range(parent_sampled, sample_rate): |
| 807 | + # type: (Optional[bool], Optional[float]) -> tuple[float, float] |
| 808 | + """ |
| 809 | + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values |
| 810 | + that a generated sample_rand value must fall into, given the parent_sampled and |
| 811 | + sample_rate values. |
| 812 | + """ |
| 813 | + if parent_sampled is None or sample_rate is None: |
| 814 | + return 0.0, 1.0 |
| 815 | + elif parent_sampled is True: |
| 816 | + return 0.0, sample_rate |
| 817 | + else: # parent_sampled is False |
| 818 | + return sample_rate, 1.0 |
| 819 | + |
| 820 | + |
751 | 821 | # Circular imports
|
752 | 822 | from sentry_sdk.tracing import (
|
753 | 823 | BAGGAGE_HEADER_NAME,
|
|
0 commit comments