Skip to content

Commit 92cd6fb

Browse files
feat(tracing): Propagate sample_rand to transaction's baggage
`continue_trace` now propagates incoming `sample_rand` values to the transaction's baggage. Also, in the case where `sample_rand` is missing from the incoming trace and needs to be backfilled, this change introduces a mechanism for the backfilled value from the scope's propagation context to be propagated to the transaction's baggage. The transaction still does not use the `sample_rand` for making sampling decisions; this PR only enables propagation. A future PR will add support for reading the incoming/backfilled `sample_rand` and for using this value to make sampling decisions. Ref #3998
1 parent bd2bfcb commit 92cd6fb

File tree

4 files changed

+103
-3
lines changed

4 files changed

+103
-3
lines changed

sentry_sdk/scope.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,8 +1122,16 @@ def continue_trace(
11221122
"""
11231123
self.generate_propagation_context(environ_or_headers)
11241124

1125+
# When we generate the propagation context, the sample_rand value is set
1126+
# if missing or invalid (we use the original value if it's valid).
1127+
# We want the transaction to use the same sample_rand value. Due to duplicated
1128+
# propagation logic in the transaction, we pass it in to avoid recomputing it
1129+
# in the transaction.
1130+
sample_rand = self._propagation_context._sample_rand()
1131+
11251132
transaction = Transaction.continue_from_headers(
11261133
normalize_incoming_data(environ_or_headers),
1134+
sample_rand=sample_rand,
11271135
op=op,
11281136
origin=origin,
11291137
name=name,

sentry_sdk/tracing.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ def continue_from_environ(
467467
def continue_from_headers(
468468
cls,
469469
headers, # type: Mapping[str, str]
470+
*,
471+
sample_rand=None, # type: Optional[str]
470472
**kwargs, # type: Any
471473
):
472474
# type: (...) -> Transaction
@@ -475,6 +477,8 @@ def continue_from_headers(
475477
the ``sentry-trace`` and ``baggage`` headers).
476478
477479
:param headers: The dictionary with the HTTP headers to pull information from.
480+
:param sample_rand: If provided, we override the sample_rand value from the
481+
incoming headers with this value.
478482
"""
479483
# TODO move this to the Transaction class
480484
if cls is Span:
@@ -485,7 +489,9 @@ def continue_from_headers(
485489

486490
# TODO-neel move away from this kwargs stuff, it's confusing and opaque
487491
# make more explicit
488-
baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME))
492+
baggage = Baggage.from_incoming_header(
493+
headers.get(BAGGAGE_HEADER_NAME), sample_rand=sample_rand
494+
)
489495
kwargs.update({BAGGAGE_HEADER_NAME: baggage})
490496

491497
sentrytrace_kwargs = extract_sentrytrace_data(

sentry_sdk/tracing_utils.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,14 @@ def _fill_sample_rand(self):
514514
random_value * factor + offset
515515
)
516516

517+
def _sample_rand(self):
518+
# type: () -> Optional[str]
519+
"""Convenience method to get the sample_rand value from the dynamic_sampling_context."""
520+
if self.dynamic_sampling_context is None:
521+
return None
522+
523+
return self.dynamic_sampling_context.get("sample_rand")
524+
517525

518526
class Baggage:
519527
"""
@@ -536,8 +544,8 @@ def __init__(
536544
self.mutable = mutable
537545

538546
@classmethod
539-
def from_incoming_header(cls, header):
540-
# type: (Optional[str]) -> Baggage
547+
def from_incoming_header(cls, header, *, sample_rand=None):
548+
# type: (Optional[str], ..., Optional[str]) -> Baggage
541549
"""
542550
freeze if incoming header already has sentry baggage
543551
"""
@@ -560,6 +568,10 @@ def from_incoming_header(cls, header):
560568
else:
561569
third_party_items += ("," if third_party_items else "") + item
562570

571+
if sample_rand is not None:
572+
sentry_items["sample_rand"] = str(sample_rand)
573+
mutable = False
574+
563575
return Baggage(sentry_items, third_party_items, mutable)
564576

565577
@classmethod
@@ -683,6 +695,9 @@ def strip_sentry_baggage(header):
683695
)
684696
)
685697

698+
def __repr__(self):
699+
return f"<Baggage {self.serialize()}>"
700+
686701

687702
def should_propagate_trace(client, url):
688703
# type: (sentry_sdk.client.BaseClient, str) -> bool
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
These tests exist to verify that Scope.continue_trace() correctly propagates the
3+
sample_rand value onto the transaction's baggage.
4+
5+
We check both the case where there is an incoming sample_rand, as well as the case
6+
where we need to compute it because it is missing.
7+
"""
8+
9+
import pytest
10+
11+
import sentry_sdk
12+
13+
14+
def test_continue_trace_with_sample_rand():
15+
"""
16+
Test that an incoming sample_rand is propagated onto the transaction's baggage.
17+
"""
18+
headers = {
19+
"sentry-trace": "00000000000000000000000000000000-0000000000000000-0",
20+
"baggage": "sentry-sample_rand=0.1,sentry-sample_rate=0.5",
21+
}
22+
23+
transaction = sentry_sdk.continue_trace(headers)
24+
assert transaction.get_baggage().sentry_items["sample_rand"] == "0.1"
25+
26+
27+
@pytest.mark.parametrize(
28+
("parent_sampled", "sample_rate", "expected_sample_rand"),
29+
(
30+
(None, None, "0.8766381713144122"),
31+
(None, "0.5", "0.8766381713144122"),
32+
(False, None, "0.8766381713144122"),
33+
(True, None, "0.8766381713144122"),
34+
(False, "0.0", "0.8766381713144122"),
35+
(False, "0.01", "0.8778717896012681"),
36+
(True, "0.01", "0.008766381713144122"),
37+
(False, "0.1", "0.888974354182971"),
38+
(True, "0.1", "0.08766381713144122"),
39+
(False, "0.5", "0.9383190856572061"),
40+
(True, "0.5", "0.4383190856572061"),
41+
(True, "1.0", "0.8766381713144122"),
42+
),
43+
)
44+
def test_continue_trace_missing_sample_rand(
45+
parent_sampled, sample_rate, expected_sample_rand
46+
):
47+
"""
48+
Test that a missing sample_rand is filled in onto the transaction's baggage. The sample_rand
49+
is pseudorandomly generated based on the trace_id, so we assert the exact values that should
50+
be generated.
51+
"""
52+
headers = {
53+
"sentry-trace": f"00000000000000000000000000000000-0000000000000000{sampled_flag(parent_sampled)}",
54+
"baggage": f"sentry-sample_rate={sample_rate}",
55+
}
56+
57+
transaction = sentry_sdk.continue_trace(headers)
58+
assert transaction.get_baggage().sentry_items["sample_rand"] == expected_sample_rand
59+
60+
61+
def sampled_flag(sampled):
62+
"""
63+
convenience function to get the sampled flag on the sentry-trace header, given a parent
64+
sampling decision.
65+
"""
66+
if sampled is None:
67+
return ""
68+
elif sampled is True:
69+
return "-1"
70+
else:
71+
return "-0"

0 commit comments

Comments
 (0)