diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 3868b2e6c8..b5592e9c29 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,7 +1,7 @@ import uuid -import random import warnings from datetime import datetime, timedelta, timezone +from random import Random import sentry_sdk from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA @@ -761,6 +761,7 @@ class Transaction(Span): "parent_sampled", # used to create baggage value for head SDKs in dynamic sampling "sample_rate", + "_sample_rand", "_measurements", "_contexts", "_profile", @@ -1152,10 +1153,9 @@ def _set_initial_sampling_decision(self, sampling_context): self.sampled = False return - # Now we roll the dice. random.random is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = random.random() < self.sample_rate + # Now we "roll the dice" by using the pre-computed sample_rand value. + # The sample_rand is in the range [0.0, 1.0). + self.sampled = self.sample_rand() < self.sample_rate if self.sampled: logger.debug( @@ -1171,6 +1171,45 @@ def _set_initial_sampling_decision(self, sampling_context): ) ) + def sample_rand(self): + # type: () -> float + """Generate a sample_rand value, or obtain it from the baggage. + + The sample_rand value is used to determine if a trace is sampled. We use the sample_rand + value from the incoming baggage header, if available. Otherwise, we generate a new one + according to the [specs](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value). + + The first time this function is called, we generate the sample_rand value. Future calls + will return the same value, since we cache the sample_rand on the transaction. + """ + cached_sample_rand = getattr(self, "_sample_rand", None) + + if cached_sample_rand is not None: + return cached_sample_rand + + incoming_sample_rand = self._incoming_sample_rand() + if incoming_sample_rand is not None: + return incoming_sample_rand + + return self._generate_sample_rand() + + def _generate_sample_rand(self): + # type: () -> float + """Generate a sample_rand value for this transaction. + + Per the [specs](https://develop.sentry.dev/sdk/telemetry/traces/#propagated-random-value), + the `sample_rand` value is a pseudo-random number in the range [0.0, 1.0), which we generate + deterministically based on the transaction's trace ID. + """ + return Random(self.trace_id).random() + + def _incoming_sample_rand(self): + # type: () -> Optional[float] + """Returns the sample_rand value from the incoming baggage header, if available.""" + if self._baggage is not None: + return self._baggage.sample_rand() + return None + class NoOpSpan(Span): def __repr__(self): diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 0459563776..8c170cd023 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -552,6 +552,7 @@ def populate_from_transaction(cls, transaction): options = client.options or {} sentry_items["trace_id"] = transaction.trace_id + sentry_items["sample_rand"] = str(transaction.sample_rand()) if options.get("environment"): sentry_items["environment"] = options["environment"] @@ -624,6 +625,24 @@ def strip_sentry_baggage(header): ) ) + def sample_rand(self): + # type: () -> Optional[float] + """Gets the sample_rand value from the baggage, if available. + + This function validates the `sample_rand` before returning it. A valid `sample_rand` is + a float in the range [0.0, 1.0). If the `sample_rand` is missing or invalid, we return + `None` instead of the invalid/missing value. + """ + try: + sample_rand = float(self.sentry_items["sample_rand"]) + except (KeyError, ValueError): + return None + + if sample_rand < 0.0 or sample_rand >= 1.0: + return None + + return sample_rand + def should_propagate_trace(client, url): # type: (sentry_sdk.client.BaseClient, str) -> bool