Skip to content

Commit 74e7c41

Browse files
feat(tracing): Use sample_rand for sampling decisions
Use the `sample_rand` value from an incoming trace to make sampling decisions, rather than generating a random value. When we are the head SDK starting a new trace, save our randomly-generated value as the `sample_rand`, and also change the random generation logic so that the `sample_rand` is computed deterministically based on the `trace_id`. Closes #3998
1 parent 61ae6e7 commit 74e7c41

File tree

11 files changed

+167
-80
lines changed

11 files changed

+167
-80
lines changed

sentry_sdk/tracing.py

Lines changed: 12 additions & 3 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
@@ -785,6 +784,7 @@ class Transaction(Span):
785784
"_profile",
786785
"_continuous_profile",
787786
"_baggage",
787+
"_sample_rand",
788788
)
789789

790790
def __init__( # type: ignore[misc]
@@ -809,6 +809,14 @@ def __init__( # type: ignore[misc]
809809
self._continuous_profile = None # type: Optional[ContinuousProfile]
810810
self._baggage = baggage
811811

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+
812820
def __repr__(self):
813821
# type: () -> str
814822
return (
@@ -1179,10 +1187,10 @@ def _set_initial_sampling_decision(self, sampling_context):
11791187
self.sampled = False
11801188
return
11811189

1182-
# 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,
11831191
# so strict < is safe here. In case sample_rate is a boolean, cast it
11841192
# to a float (True becomes 1.0 and False becomes 0.0)
1185-
self.sampled = random.random() < self.sample_rate
1193+
self.sampled = self._sample_rand < self.sample_rate
11861194

11871195
if self.sampled:
11881196
logger.debug(
@@ -1339,6 +1347,7 @@ async def my_async_function():
13391347
Baggage,
13401348
EnvironHeaders,
13411349
extract_sentrytrace_data,
1350+
_generate_sample_rand,
13421351
has_tracing_enabled,
13431352
maybe_create_breadcrumbs_from_span,
13441353
)

sentry_sdk/tracing_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ def populate_from_transaction(cls, transaction):
645645
options = client.options or {}
646646

647647
sentry_items["trace_id"] = transaction.trace_id
648+
sentry_items["sample_rand"] = str(transaction._sample_rand)
648649

649650
if options.get("environment"):
650651
sentry_items["environment"] = options["environment"]
@@ -717,6 +718,20 @@ def strip_sentry_baggage(header):
717718
)
718719
)
719720

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+
720735
def __repr__(self):
721736
# type: () -> str
722737
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():
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=transaction.trace_id",
524+
"sentry-environment=production",
525+
"sentry-sample_rand=0.5",
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_monitor.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import random
21
from collections import Counter
32
from unittest import mock
43

@@ -68,17 +67,16 @@ def test_transaction_uses_downsampled_rate(
6867
monitor = sentry_sdk.get_client().monitor
6968
monitor.interval = 0.1
7069

71-
# make sure rng doesn't sample
72-
monkeypatch.setattr(random, "random", lambda: 0.9)
73-
7470
assert monitor.is_healthy() is True
7571
monitor.run()
7672
assert monitor.is_healthy() is False
7773
assert monitor.downsample_factor == 1
7874

79-
with sentry_sdk.start_transaction(name="foobar") as transaction:
80-
assert transaction.sampled is False
81-
assert transaction.sample_rate == 0.5
75+
# make sure we don't sample the transaction
76+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.75):
77+
with sentry_sdk.start_transaction(name="foobar") as transaction:
78+
assert transaction.sampled is False
79+
assert transaction.sample_rate == 0.5
8280

8381
assert Counter(record_lost_event_calls) == Counter(
8482
[

tests/tracing/test_integration_tests.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import gc
2-
import random
32
import re
43
import sys
54
import weakref
5+
from unittest import mock
66

77
import pytest
88

@@ -169,9 +169,8 @@ def test_dynamic_sampling_head_sdk_creates_dsc(
169169
envelopes = capture_envelopes()
170170

171171
# make sure transaction is sampled for both cases
172-
monkeypatch.setattr(random, "random", lambda: 0.1)
173-
174-
transaction = Transaction.continue_from_headers({}, name="Head SDK tx")
172+
with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25):
173+
transaction = Transaction.continue_from_headers({}, name="Head SDK tx")
175174

176175
# will create empty mutable baggage
177176
baggage = transaction._baggage
@@ -196,12 +195,14 @@ def test_dynamic_sampling_head_sdk_creates_dsc(
196195
"release": "foo",
197196
"sample_rate": str(sample_rate),
198197
"sampled": "true" if transaction.sampled else "false",
198+
"sample_rand": "0.250000",
199199
"transaction": "Head SDK tx",
200200
"trace_id": trace_id,
201201
}
202202

203203
expected_baggage = (
204204
"sentry-trace_id=%s,"
205+
"sentry-sample_rand=0.250000,"
205206
"sentry-environment=production,"
206207
"sentry-release=foo,"
207208
"sentry-transaction=Head%%20SDK%%20tx,"
@@ -217,6 +218,7 @@ def test_dynamic_sampling_head_sdk_creates_dsc(
217218
"environment": "production",
218219
"release": "foo",
219220
"sample_rate": str(sample_rate),
221+
"sample_rand": "0.250000",
220222
"sampled": "true" if transaction.sampled else "false",
221223
"transaction": "Head SDK tx",
222224
"trace_id": trace_id,

0 commit comments

Comments
 (0)