|
1 | 1 | import contextlib
|
| 2 | +from decimal import ROUND_DOWN, Decimal |
2 | 3 | import inspect
|
3 | 4 | import os
|
4 | 5 | import re
|
5 | 6 | import sys
|
6 | 7 | from collections.abc import Mapping
|
7 | 8 | from datetime import timedelta
|
8 | 9 | from functools import wraps
|
| 10 | +from random import Random |
9 | 11 | from urllib.parse import quote, unquote
|
10 | 12 | import uuid
|
11 | 13 |
|
|
19 | 21 | match_regex_list,
|
20 | 22 | qualname_from_function,
|
21 | 23 | to_string,
|
| 24 | + try_convert, |
22 | 25 | is_sentry_url,
|
23 | 26 | _is_external_source,
|
24 | 27 | _is_in_project_root,
|
|
45 | 48 | "[ \t]*$" # whitespace
|
46 | 49 | )
|
47 | 50 |
|
| 51 | + |
48 | 52 | # This is a normal base64 regex, modified to reflect that fact that we strip the
|
49 | 53 | # trailing = or == off
|
50 | 54 | base64_stripped = (
|
@@ -418,13 +422,17 @@ def from_incoming_data(cls, incoming_data):
|
418 | 422 | propagation_context = PropagationContext()
|
419 | 423 | propagation_context.update(sentrytrace_data)
|
420 | 424 |
|
| 425 | + if propagation_context is not None: |
| 426 | + propagation_context._fill_sample_rand() |
| 427 | + |
421 | 428 | return propagation_context
|
422 | 429 |
|
423 | 430 | @property
|
424 | 431 | def trace_id(self):
|
425 | 432 | # type: () -> str
|
426 | 433 | """The trace id of the Sentry trace."""
|
427 | 434 | if not self._trace_id:
|
| 435 | + # New trace, don't fill in sample_rand |
428 | 436 | self._trace_id = uuid.uuid4().hex
|
429 | 437 |
|
430 | 438 | return self._trace_id
|
@@ -469,6 +477,60 @@ def __repr__(self):
|
469 | 477 | self.dynamic_sampling_context,
|
470 | 478 | )
|
471 | 479 |
|
| 480 | + def _fill_sample_rand(self): |
| 481 | + # type: () -> None |
| 482 | + """ |
| 483 | + Ensure that there is a valid sample_rand value in the dynamic_sampling_context. |
| 484 | +
|
| 485 | + If there is a valid sample_rand value in the dynamic_sampling_context, we keep it. |
| 486 | + Otherwise, we generate a sample_rand value according to the following: |
| 487 | +
|
| 488 | + - If we have a parent_sampled value and a sample_rate in the DSC, we compute |
| 489 | + a sample_rand value randomly in the range: |
| 490 | + - [0, sample_rate) if parent_sampled is True, |
| 491 | + - or, in the range [sample_rate, 1) if parent_sampled is False. |
| 492 | +
|
| 493 | + - If either parent_sampled or sample_rate is missing, we generate a random |
| 494 | + value in the range [0, 1). |
| 495 | +
|
| 496 | + The sample_rand is deterministically generated from the trace_id, if present. |
| 497 | +
|
| 498 | + This function does nothing if there is no dynamic_sampling_context. |
| 499 | + """ |
| 500 | + if self.dynamic_sampling_context is None: |
| 501 | + return |
| 502 | + |
| 503 | + sample_rand = try_convert( |
| 504 | + Decimal, self.dynamic_sampling_context.get("sample_rand") |
| 505 | + ) |
| 506 | + if sample_rand is not None and 0 <= sample_rand < 1: |
| 507 | + # sample_rand is present and valid, so don't overwrite it |
| 508 | + return |
| 509 | + |
| 510 | + # Get the sample rate and compute the transformation that will map the random value |
| 511 | + # to the desired range: [0, 1), [0, sample_rate), or [sample_rate, 1). |
| 512 | + sample_rate = try_convert( |
| 513 | + float, self.dynamic_sampling_context.get("sample_rate") |
| 514 | + ) |
| 515 | + lower, upper = _sample_rand_range(self.parent_sampled, sample_rate) |
| 516 | + |
| 517 | + try: |
| 518 | + sample_rand = _generate_sample_rand(self.trace_id, interval=(lower, upper)) |
| 519 | + except ValueError: |
| 520 | + # ValueError is raised if the interval is invalid, i.e. lower >= upper. |
| 521 | + # lower >= upper might happen if the incoming trace's sampled flag |
| 522 | + # and sample_rate are inconsistent, e.g. sample_rate=0.0 but sampled=True. |
| 523 | + # We cannot generate a sensible sample_rand value in this case. |
| 524 | + logger.debug( |
| 525 | + f"Could not backfill sample_rand, since parent_sampled={self.parent_sampled} " |
| 526 | + f"and sample_rate={sample_rate}." |
| 527 | + ) |
| 528 | + return |
| 529 | + |
| 530 | + self.dynamic_sampling_context["sample_rand"] = ( |
| 531 | + f"{sample_rand:.6f}" # noqa: E231 |
| 532 | + ) |
| 533 | + |
472 | 534 |
|
473 | 535 | class Baggage:
|
474 | 536 | """
|
@@ -748,6 +810,49 @@ def get_current_span(scope=None):
|
748 | 810 | return current_span
|
749 | 811 |
|
750 | 812 |
|
| 813 | +def _generate_sample_rand( |
| 814 | + trace_id, # type: Optional[str] |
| 815 | + *, |
| 816 | + interval=(0.0, 1.0), # type: tuple[float, float] |
| 817 | +): |
| 818 | + # type: (...) -> Decimal |
| 819 | + """Generate a sample_rand value from a trace ID. |
| 820 | +
|
| 821 | + The generated value will be pseudorandomly chosen from the provided |
| 822 | + interval. Specifically, given (lower, upper) = interval, the generated |
| 823 | + value will be in the range [lower, upper). The value has 6-digit precision, |
| 824 | + so when printing with .6f, the value will never be rounded up. |
| 825 | +
|
| 826 | + The pseudorandom number generator is seeded with the trace ID. |
| 827 | + """ |
| 828 | + lower, upper = interval |
| 829 | + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly |
| 830 | + raise ValueError("Invalid interval: lower must be less than upper") |
| 831 | + |
| 832 | + rng = Random(trace_id) |
| 833 | + sample_rand = upper |
| 834 | + while sample_rand >= upper: |
| 835 | + sample_rand = rng.uniform(lower, upper) |
| 836 | + |
| 837 | + # Round down to exactly six decimal-digit precision. |
| 838 | + return Decimal(sample_rand).quantize(Decimal("0.000001"), rounding=ROUND_DOWN) |
| 839 | + |
| 840 | + |
| 841 | +def _sample_rand_range(parent_sampled, sample_rate): |
| 842 | + # type: (Optional[bool], Optional[float]) -> tuple[float, float] |
| 843 | + """ |
| 844 | + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values |
| 845 | + that a generated sample_rand value must fall into, given the parent_sampled and |
| 846 | + sample_rate values. |
| 847 | + """ |
| 848 | + if parent_sampled is None or sample_rate is None: |
| 849 | + return 0.0, 1.0 |
| 850 | + elif parent_sampled is True: |
| 851 | + return 0.0, sample_rate |
| 852 | + else: # parent_sampled is False |
| 853 | + return sample_rate, 1.0 |
| 854 | + |
| 855 | + |
751 | 856 | # Circular imports
|
752 | 857 | from sentry_sdk.tracing import (
|
753 | 858 | BAGGAGE_HEADER_NAME,
|
|
0 commit comments