Skip to content

Commit 8cdcb70

Browse files
feat(tracing): Propagate sample_rand to transaction's baggage (#4040)
`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. Depends on: - #4038 Ref #3998
1 parent 789b065 commit 8cdcb70

File tree

14 files changed

+250
-85
lines changed

14 files changed

+250
-85
lines changed

sentry_sdk/scope.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
logger,
4444
)
4545

46+
import typing
4647
from typing import TYPE_CHECKING
4748

4849
if TYPE_CHECKING:
@@ -1146,8 +1147,20 @@ def continue_trace(
11461147
"""
11471148
self.generate_propagation_context(environ_or_headers)
11481149

1150+
# When we generate the propagation context, the sample_rand value is set
1151+
# if missing or invalid (we use the original value if it's valid).
1152+
# We want the transaction to use the same sample_rand value. Due to duplicated
1153+
# propagation logic in the transaction, we pass it in to avoid recomputing it
1154+
# in the transaction.
1155+
# TYPE SAFETY: self.generate_propagation_context() ensures that self._propagation_context
1156+
# is not None.
1157+
sample_rand = typing.cast(
1158+
PropagationContext, self._propagation_context
1159+
)._sample_rand()
1160+
11491161
transaction = Transaction.continue_from_headers(
11501162
normalize_incoming_data(environ_or_headers),
1163+
_sample_rand=sample_rand,
11511164
op=op,
11521165
origin=origin,
11531166
name=name,

sentry_sdk/tracing.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import uuid
2-
import random
32
import warnings
43
from datetime import datetime, timedelta, timezone
54
from enum import Enum
@@ -477,6 +476,8 @@ def continue_from_environ(
477476
def continue_from_headers(
478477
cls,
479478
headers, # type: Mapping[str, str]
479+
*,
480+
_sample_rand=None, # type: Optional[str]
480481
**kwargs, # type: Any
481482
):
482483
# type: (...) -> Transaction
@@ -485,6 +486,8 @@ def continue_from_headers(
485486
the ``sentry-trace`` and ``baggage`` headers).
486487
487488
:param headers: The dictionary with the HTTP headers to pull information from.
489+
:param _sample_rand: If provided, we override the sample_rand value from the
490+
incoming headers with this value. (internal use only)
488491
"""
489492
# TODO move this to the Transaction class
490493
if cls is Span:
@@ -495,7 +498,9 @@ def continue_from_headers(
495498

496499
# TODO-neel move away from this kwargs stuff, it's confusing and opaque
497500
# make more explicit
498-
baggage = Baggage.from_incoming_header(headers.get(BAGGAGE_HEADER_NAME))
501+
baggage = Baggage.from_incoming_header(
502+
headers.get(BAGGAGE_HEADER_NAME), _sample_rand=_sample_rand
503+
)
499504
kwargs.update({BAGGAGE_HEADER_NAME: baggage})
500505

501506
sentrytrace_kwargs = extract_sentrytrace_data(
@@ -779,6 +784,7 @@ class Transaction(Span):
779784
"_profile",
780785
"_continuous_profile",
781786
"_baggage",
787+
"_sample_rand",
782788
)
783789

784790
def __init__( # type: ignore[misc]
@@ -803,6 +809,14 @@ def __init__( # type: ignore[misc]
803809
self._continuous_profile = None # type: Optional[ContinuousProfile]
804810
self._baggage = baggage
805811

812+
baggage_sample_rand = (
813+
None if self._baggage is None else self._baggage._sample_rand()
814+
)
815+
if baggage_sample_rand is not None:
816+
self._sample_rand = baggage_sample_rand
817+
else:
818+
self._sample_rand = _generate_sample_rand(self.trace_id)
819+
806820
def __repr__(self):
807821
# type: () -> str
808822
return (
@@ -1173,10 +1187,10 @@ def _set_initial_sampling_decision(self, sampling_context):
11731187
self.sampled = False
11741188
return
11751189

1176-
# Now we roll the dice. random.random is inclusive of 0, but not of 1,
1190+
# Now we roll the dice. self._sample_rand is inclusive of 0, but not of 1,
11771191
# so strict < is safe here. In case sample_rate is a boolean, cast it
11781192
# to a float (True becomes 1.0 and False becomes 0.0)
1179-
self.sampled = random.random() < self.sample_rate
1193+
self.sampled = self._sample_rand < self.sample_rate
11801194

11811195
if self.sampled:
11821196
logger.debug(
@@ -1333,6 +1347,7 @@ async def my_async_function():
13331347
Baggage,
13341348
EnvironHeaders,
13351349
extract_sentrytrace_data,
1350+
_generate_sample_rand,
13361351
has_tracing_enabled,
13371352
maybe_create_breadcrumbs_from_span,
13381353
)

sentry_sdk/tracing_utils.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,14 @@ def _fill_sample_rand(self):
531531
f"{sample_rand:.6f}" # noqa: E231
532532
)
533533

534+
def _sample_rand(self):
535+
# type: () -> Optional[str]
536+
"""Convenience method to get the sample_rand value from the dynamic_sampling_context."""
537+
if self.dynamic_sampling_context is None:
538+
return None
539+
540+
return self.dynamic_sampling_context.get("sample_rand")
541+
534542

535543
class Baggage:
536544
"""
@@ -553,8 +561,13 @@ def __init__(
553561
self.mutable = mutable
554562

555563
@classmethod
556-
def from_incoming_header(cls, header):
557-
# type: (Optional[str]) -> Baggage
564+
def from_incoming_header(
565+
cls,
566+
header, # type: Optional[str]
567+
*,
568+
_sample_rand=None, # type: Optional[str]
569+
):
570+
# type: (...) -> Baggage
558571
"""
559572
freeze if incoming header already has sentry baggage
560573
"""
@@ -577,6 +590,10 @@ def from_incoming_header(cls, header):
577590
else:
578591
third_party_items += ("," if third_party_items else "") + item
579592

593+
if _sample_rand is not None:
594+
sentry_items["sample_rand"] = str(_sample_rand)
595+
mutable = False
596+
580597
return Baggage(sentry_items, third_party_items, mutable)
581598

582599
@classmethod
@@ -628,6 +645,7 @@ def populate_from_transaction(cls, transaction):
628645
options = client.options or {}
629646

630647
sentry_items["trace_id"] = transaction.trace_id
648+
sentry_items["sample_rand"] = str(transaction._sample_rand)
631649

632650
if options.get("environment"):
633651
sentry_items["environment"] = options["environment"]
@@ -700,6 +718,20 @@ def strip_sentry_baggage(header):
700718
)
701719
)
702720

721+
def _sample_rand(self):
722+
# type: () -> Optional[Decimal]
723+
"""Convenience method to get the sample_rand value from the sentry_items.
724+
725+
We validate the value and parse it as a Decimal before returning it. The value is considered
726+
valid if it is a Decimal in the range [0, 1).
727+
"""
728+
sample_rand = try_convert(Decimal, self.sentry_items.get("sample_rand"))
729+
730+
if sample_rand is not None and Decimal(0) <= sample_rand < Decimal(1):
731+
return sample_rand
732+
733+
return None
734+
703735
def __repr__(self):
704736
# type: () -> str
705737
return f'<Baggage "{self.serialize(include_third_party=True)}", mutable={self.mutable}>'

tests/integrations/aiohttp/test_aiohttp.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -626,18 +626,19 @@ async def handler(request):
626626

627627
raw_server = await aiohttp_raw_server(handler)
628628

629-
with start_transaction(
630-
name="/interactions/other-dogs/new-dog",
631-
op="greeting.sniff",
632-
trace_id="0123456789012345678901234567890",
633-
):
634-
client = await aiohttp_client(raw_server)
635-
resp = await client.get("/", headers={"bagGage": "custom=value"})
636-
637-
assert (
638-
resp.request_info.headers["baggage"]
639-
== "custom=value,sentry-trace_id=0123456789012345678901234567890,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
640-
)
629+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
630+
with start_transaction(
631+
name="/interactions/other-dogs/new-dog",
632+
op="greeting.sniff",
633+
trace_id="0123456789012345678901234567890",
634+
):
635+
client = await aiohttp_client(raw_server)
636+
resp = await client.get("/", headers={"bagGage": "custom=value"})
637+
638+
assert (
639+
resp.request_info.headers["baggage"]
640+
== "custom=value,sentry-trace_id=0123456789012345678901234567890,sentry-sample_rand=0.500000,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
641+
)
641642

642643

643644
@pytest.mark.asyncio

tests/integrations/celery/test_celery.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -509,22 +509,25 @@ def test_baggage_propagation(init_celery):
509509
def dummy_task(self, x, y):
510510
return _get_headers(self)
511511

512-
with start_transaction() as transaction:
513-
result = dummy_task.apply_async(
514-
args=(1, 0),
515-
headers={"baggage": "custom=value"},
516-
).get()
517-
518-
assert sorted(result["baggage"].split(",")) == sorted(
519-
[
520-
"sentry-release=abcdef",
521-
"sentry-trace_id={}".format(transaction.trace_id),
522-
"sentry-environment=production",
523-
"sentry-sample_rate=1.0",
524-
"sentry-sampled=true",
525-
"custom=value",
526-
]
527-
)
512+
# patch random.uniform to return a predictable sample_rand value
513+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
514+
with start_transaction() as transaction:
515+
result = dummy_task.apply_async(
516+
args=(1, 0),
517+
headers={"baggage": "custom=value"},
518+
).get()
519+
520+
assert sorted(result["baggage"].split(",")) == sorted(
521+
[
522+
"sentry-release=abcdef",
523+
"sentry-trace_id={}".format(transaction.trace_id),
524+
"sentry-environment=production",
525+
"sentry-sample_rand=0.500000",
526+
"sentry-sample_rate=1.0",
527+
"sentry-sampled=true",
528+
"custom=value",
529+
]
530+
)
528531

529532

530533
def test_sentry_propagate_traces_override(init_celery):

tests/integrations/httpx/test_httpx.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -170,30 +170,32 @@ def test_outgoing_trace_headers_append_to_baggage(
170170

171171
url = "http://example.com/"
172172

173-
with start_transaction(
174-
name="/interactions/other-dogs/new-dog",
175-
op="greeting.sniff",
176-
trace_id="01234567890123456789012345678901",
177-
) as transaction:
178-
if asyncio.iscoroutinefunction(httpx_client.get):
179-
response = asyncio.get_event_loop().run_until_complete(
180-
httpx_client.get(url, headers={"baGGage": "custom=data"})
173+
# patch random.uniform to return a predictable sample_rand value
174+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5):
175+
with start_transaction(
176+
name="/interactions/other-dogs/new-dog",
177+
op="greeting.sniff",
178+
trace_id="01234567890123456789012345678901",
179+
) as transaction:
180+
if asyncio.iscoroutinefunction(httpx_client.get):
181+
response = asyncio.get_event_loop().run_until_complete(
182+
httpx_client.get(url, headers={"baGGage": "custom=data"})
183+
)
184+
else:
185+
response = httpx_client.get(url, headers={"baGGage": "custom=data"})
186+
187+
request_span = transaction._span_recorder.spans[-1]
188+
assert response.request.headers[
189+
"sentry-trace"
190+
] == "{trace_id}-{parent_span_id}-{sampled}".format(
191+
trace_id=transaction.trace_id,
192+
parent_span_id=request_span.span_id,
193+
sampled=1,
194+
)
195+
assert (
196+
response.request.headers["baggage"]
197+
== "custom=data,sentry-trace_id=01234567890123456789012345678901,sentry-sample_rand=0.500000,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
181198
)
182-
else:
183-
response = httpx_client.get(url, headers={"baGGage": "custom=data"})
184-
185-
request_span = transaction._span_recorder.spans[-1]
186-
assert response.request.headers[
187-
"sentry-trace"
188-
] == "{trace_id}-{parent_span_id}-{sampled}".format(
189-
trace_id=transaction.trace_id,
190-
parent_span_id=request_span.span_id,
191-
sampled=1,
192-
)
193-
assert (
194-
response.request.headers["baggage"]
195-
== "custom=data,sentry-trace_id=01234567890123456789012345678901,sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true"
196-
)
197199

198200

199201
@pytest.mark.parametrize(

tests/integrations/stdlib/test_httplib.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import random
21
from http.client import HTTPConnection, HTTPSConnection
32
from socket import SocketIO
43
from urllib.error import HTTPError
@@ -189,7 +188,7 @@ def test_outgoing_trace_headers(sentry_init, monkeypatch):
189188
"baggage": (
190189
"other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, "
191190
"sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, "
192-
"sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;"
191+
"sentry-user_id=Am%C3%A9lie, sentry-sample_rand=0.132521102938283, other-vendor-value-2=foo;bar;"
193192
),
194193
}
195194

@@ -222,7 +221,8 @@ def test_outgoing_trace_headers(sentry_init, monkeypatch):
222221
"sentry-trace_id=771a43a4192642f0b136d5159a501700,"
223222
"sentry-public_key=49d0f7386ad645858ae85020e393bef3,"
224223
"sentry-sample_rate=1.0,"
225-
"sentry-user_id=Am%C3%A9lie"
224+
"sentry-user_id=Am%C3%A9lie,"
225+
"sentry-sample_rand=0.132521102938283"
226226
)
227227

228228
assert request_headers["baggage"] == expected_outgoing_baggage
@@ -235,11 +235,9 @@ def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
235235
mock_send = mock.Mock()
236236
monkeypatch.setattr(HTTPSConnection, "send", mock_send)
237237

238-
# make sure transaction is always sampled
239-
monkeypatch.setattr(random, "random", lambda: 0.1)
240-
241238
sentry_init(traces_sample_rate=0.5, release="foo")
242-
transaction = Transaction.continue_from_headers({})
239+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25):
240+
transaction = Transaction.continue_from_headers({})
243241

244242
with start_transaction(transaction=transaction, name="Head SDK tx") as transaction:
245243
HTTPSConnection("www.squirrelchasers.com").request("GET", "/top-chasers")
@@ -261,6 +259,7 @@ def test_outgoing_trace_headers_head_sdk(sentry_init, monkeypatch):
261259

262260
expected_outgoing_baggage = (
263261
"sentry-trace_id=%s,"
262+
"sentry-sample_rand=0.250000,"
264263
"sentry-environment=production,"
265264
"sentry-release=foo,"
266265
"sentry-sample_rate=0.5,"

tests/test_api.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import pytest
2+
3+
import re
24
from unittest import mock
35

46
import sentry_sdk
@@ -95,10 +97,10 @@ def test_baggage_with_tracing_disabled(sentry_init):
9597
def test_baggage_with_tracing_enabled(sentry_init):
9698
sentry_init(traces_sample_rate=1.0, release="1.0.0", environment="dev")
9799
with start_transaction() as transaction:
98-
expected_baggage = "sentry-trace_id={},sentry-environment=dev,sentry-release=1.0.0,sentry-sample_rate=1.0,sentry-sampled={}".format(
100+
expected_baggage_re = r"^sentry-trace_id={},sentry-sample_rand=0\.\d{{6}},sentry-environment=dev,sentry-release=1\.0\.0,sentry-sample_rate=1\.0,sentry-sampled={}$".format(
99101
transaction.trace_id, "true" if transaction.sampled else "false"
100102
)
101-
assert get_baggage() == expected_baggage
103+
assert re.match(expected_baggage_re, get_baggage())
102104

103105

104106
@pytest.mark.forked

tests/test_dsc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
This is not tested in this file.
99
"""
1010

11-
import random
1211
from unittest import mock
1312

1413
import pytest
@@ -176,7 +175,7 @@ def my_traces_sampler(sampling_context):
176175
}
177176

178177
# We continue the incoming trace and start a new transaction
179-
with mock.patch.object(random, "random", return_value=0.2):
178+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.125):
180179
transaction = sentry_sdk.continue_trace(incoming_http_headers)
181180
with sentry_sdk.start_transaction(transaction, name="foo"):
182181
pass

0 commit comments

Comments
 (0)