diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 744003eaba..e2998cf164 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,6 +1,6 @@ +from datetime import datetime from enum import Enum import json -from datetime import datetime from opentelemetry import trace as otel_trace, context from opentelemetry.trace import ( diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 6b57ae54ba..3afc43f28b 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -7,6 +7,7 @@ from collections.abc import Mapping from datetime import datetime, timedelta, timezone from functools import wraps +from random import Random from urllib.parse import quote, unquote import sentry_sdk @@ -44,6 +45,7 @@ "[ \t]*$" # whitespace ) + # This is a normal base64 regex, modified to reflect that fact that we strip the # trailing = or == off base64_stripped = ( @@ -466,8 +468,11 @@ def __init__( self.mutable = mutable @classmethod - def from_incoming_header(cls, header): - # type: (Optional[str]) -> Baggage + def from_incoming_header( + cls, + header, # type: Optional[str] + ): + # type: (...) -> Baggage """ freeze if incoming header already has sentry baggage """ @@ -677,6 +682,55 @@ def get_current_span(scope=None): return current_span +# XXX-potel-ivana: use this +def _generate_sample_rand( + trace_id, # type: Optional[str] + *, + interval=(0.0, 1.0), # type: tuple[float, float] +): + # type: (...) -> Any + """Generate a sample_rand value from a trace ID. + + The generated value will be pseudorandomly chosen from the provided + interval. Specifically, given (lower, upper) = interval, the generated + value will be in the range [lower, upper). The value has 6-digit precision, + so when printing with .6f, the value will never be rounded up. + + The pseudorandom number generator is seeded with the trace ID. + """ + import decimal + + lower, upper = interval + if not lower < upper: # using `if lower >= upper` would handle NaNs incorrectly + raise ValueError("Invalid interval: lower must be less than upper") + + rng = Random(trace_id) + sample_rand = upper + while sample_rand >= upper: + sample_rand = rng.uniform(lower, upper) + + # Round down to exactly six decimal-digit precision. + return decimal.Decimal(sample_rand).quantize( + decimal.Decimal("0.000001"), rounding=decimal.ROUND_DOWN + ) + + +# XXX-potel-ivana: use this +def _sample_rand_range(parent_sampled, sample_rate): + # type: (Optional[bool], Optional[float]) -> tuple[float, float] + """ + Compute the lower (inclusive) and upper (exclusive) bounds of the range of values + that a generated sample_rand value must fall into, given the parent_sampled and + sample_rate values. + """ + if parent_sampled is None or sample_rate is None: + return 0.0, 1.0 + elif parent_sampled is True: + return 0.0, sample_rate + else: # parent_sampled is False + return sample_rate, 1.0 + + # Circular imports from sentry_sdk.tracing import ( BAGGAGE_HEADER_NAME, diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index f5fa766eae..93560421c0 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -634,24 +634,26 @@ async def handler(request): raw_server = await aiohttp_raw_server(handler) - with start_span( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - ) as transaction: - client = await aiohttp_client(raw_server) - resp = await client.get("/", headers={"bagGage": "custom=value"}) - - assert sorted(resp.request_info.headers["baggage"].split(",")) == sorted( - [ - "custom=value", - f"sentry-trace_id={transaction.trace_id}", - "sentry-environment=production", - "sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42", - "sentry-transaction=/interactions/other-dogs/new-dog", - "sentry-sample_rate=1.0", - "sentry-sampled=true", - ] - ) + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + with start_span( + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + ) as transaction: + client = await aiohttp_client(raw_server) + resp = await client.get("/", headers={"bagGage": "custom=value"}) + + assert sorted(resp.request_info.headers["baggage"].split(",")) == sorted( + [ + "custom=value", + f"sentry-trace_id={transaction.trace_id}", + "sentry-environment=production", + "sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42", + "sentry-transaction=/interactions/other-dogs/new-dog", + "sentry-sample_rate=1.0", + "sentry-sampled=true", + "sentry-sample_rand=0.500000", + ] + ) @pytest.mark.asyncio diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index c9a110af9e..2f25e13a60 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -509,23 +509,25 @@ def test_baggage_propagation(init_celery): def dummy_task(self, x, y): return _get_headers(self) - with sentry_sdk.start_span(name="task") as root_span: - result = dummy_task.apply_async( - args=(1, 0), - headers={"baggage": "custom=value"}, - ).get() - - assert sorted(result["baggage"].split(",")) == sorted( - [ - "sentry-release=abcdef", - "sentry-trace_id={}".format(root_span.trace_id), - "sentry-transaction=task", - "sentry-environment=production", - "sentry-sample_rate=1.0", - "sentry-sampled=true", - "custom=value", - ] - ) + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + with sentry_sdk.start_span(name="task") as root_span: + result = dummy_task.apply_async( + args=(1, 0), + headers={"baggage": "custom=value"}, + ).get() + + assert sorted(result["baggage"].split(",")) == sorted( + [ + "sentry-release=abcdef", + "sentry-trace_id={}".format(root_span.trace_id), + "sentry-transaction=task", + "sentry-environment=production", + "sentry-sample_rand=0.500000", + "sentry-sample_rate=1.0", + "sentry-sampled=true", + "custom=value", + ] + ) def test_sentry_propagate_traces_override(init_celery): diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index e5ebb19bde..9e4b140f70 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -174,32 +174,33 @@ def test_outgoing_trace_headers_append_to_baggage( url = "http://example.com/" - with start_span( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - ): - if asyncio.iscoroutinefunction(httpx_client.get): - response = asyncio.get_event_loop().run_until_complete( - httpx_client.get(url, headers={"baGGage": "custom=data"}) - ) - else: - response = httpx_client.get(url, headers={"baGGage": "custom=data"}) - - (envelope,) = envelopes - transaction = envelope.get_transaction_event() - request_span = transaction["spans"][-1] - trace_id = transaction["contexts"]["trace"]["trace_id"] - - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=trace_id, - parent_span_id=request_span["span_id"], - sampled=1, - ) - assert response.request.headers["baggage"] == SortedBaggage( - f"custom=data,sentry-trace_id={trace_id},sentry-environment=production,sentry-release=d08ebdb9309e1b004c6f52202de58a09c2268e42,sentry-transaction=/interactions/other-dogs/new-dog,sentry-sample_rate=1.0,sentry-sampled=true" # noqa: E231 - ) + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.5): + with start_span( + name="/interactions/other-dogs/new-dog", + op="greeting.sniff", + ): + if asyncio.iscoroutinefunction(httpx_client.get): + response = asyncio.get_event_loop().run_until_complete( + httpx_client.get(url, headers={"baGGage": "custom=data"}) + ) + else: + response = httpx_client.get(url, headers={"baGGage": "custom=data"}) + + (envelope,) = envelopes + transaction = envelope.get_transaction_event() + request_span = transaction["spans"][-1] + trace_id = transaction["contexts"]["trace"]["trace_id"] + + assert response.request.headers[ + "sentry-trace" + ] == "{trace_id}-{parent_span_id}-{sampled}".format( + trace_id=trace_id, + parent_span_id=request_span["span_id"], + sampled=1, + ) + assert response.request.headers["baggage"] == SortedBaggage( + f"custom=data,sentry-trace_id={trace_id},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" # noqa: E231 + ) @pytest.mark.parametrize( diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 1cad653558..56a0f85168 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -1,4 +1,3 @@ -import random from http.client import HTTPConnection, HTTPSConnection from socket import SocketIO from urllib.error import HTTPError @@ -207,7 +206,7 @@ def test_outgoing_trace_headers( "baggage": ( "other-vendor-value-1=foo;bar;baz, sentry-trace_id=771a43a4192642f0b136d5159a501700, " "sentry-public_key=49d0f7386ad645858ae85020e393bef3, sentry-sample_rate=0.01337, " - "sentry-user_id=Am%C3%A9lie, other-vendor-value-2=foo;bar;" + "sentry-user_id=Am%C3%A9lie, sentry-sample_rand=0.132521102938283, other-vendor-value-2=foo;bar;" ), } @@ -233,28 +232,27 @@ def test_outgoing_trace_headers( "sentry-trace_id=771a43a4192642f0b136d5159a501700," "sentry-public_key=49d0f7386ad645858ae85020e393bef3," "sentry-sample_rate=1.0," - "sentry-user_id=Am%C3%A9lie" + "sentry-user_id=Am%C3%A9lie," + "sentry-sample_rand=0.132521102938283" ) assert request_headers["baggage"] == SortedBaggage(expected_outgoing_baggage) def test_outgoing_trace_headers_head_sdk( - sentry_init, monkeypatch, capture_request_headers, capture_envelopes + sentry_init, capture_request_headers, capture_envelopes ): - # make sure transaction is always sampled - monkeypatch.setattr(random, "random", lambda: 0.1) - sentry_init(traces_sample_rate=0.5, release="foo") envelopes = capture_envelopes() request_headers = capture_request_headers() - with isolation_scope(): - with continue_trace({}): - with start_span(name="Head SDK tx") as root_span: - conn = HTTPConnection("localhost", PORT) - conn.request("GET", "/top-chasers") - conn.getresponse() + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25): + with isolation_scope(): + with continue_trace({}): + with start_span(name="Head SDK tx") as root_span: + conn = HTTPConnection("localhost", PORT) + conn.request("GET", "/top-chasers") + conn.getresponse() (envelope,) = envelopes transaction = envelope.get_transaction_event() @@ -269,6 +267,7 @@ def test_outgoing_trace_headers_head_sdk( expected_outgoing_baggage = ( f"sentry-trace_id={root_span.trace_id}," # noqa: E231 + "sentry-sample_rand=0.250000," "sentry-environment=production," "sentry-release=foo," "sentry-sample_rate=0.5," diff --git a/tests/test_api.py b/tests/test_api.py index 40bc673dda..612a092a73 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,4 +1,6 @@ import pytest + +import re from unittest import mock from sentry_sdk import ( @@ -93,10 +95,10 @@ def test_baggage_with_tracing_disabled(sentry_init): def test_baggage_with_tracing_enabled(sentry_init): sentry_init(traces_sample_rate=1.0, release="1.0.0", environment="dev") with start_span(name="foo") as span: - expected_baggage = "sentry-transaction=foo,sentry-trace_id={},sentry-environment=dev,sentry-release=1.0.0,sentry-sample_rate=1.0,sentry-sampled={}".format( + expected_baggage_re = r"^sentry-transaction=foo,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( span.trace_id, "true" if span.sampled else "false" ) - assert get_baggage() == SortedBaggage(expected_baggage) + assert re.match(expected_baggage_re, get_baggage()) @pytest.mark.forked @@ -110,18 +112,18 @@ def test_continue_trace(sentry_init): with continue_trace( { "sentry-trace": "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled), - "baggage": "sentry-trace_id=566e3688a61d4bc888951642d6f14a19", + "baggage": "sentry-trace_id=566e3688a61d4bc888951642d6f14a19,sentry-sample_rand=0.123456", }, ): with start_span(name="some name") as span: assert span.name == "some name" - propagation_context = get_isolation_scope()._propagation_context assert propagation_context.trace_id == span.trace_id == trace_id assert propagation_context.parent_span_id == parent_span_id assert propagation_context.parent_sampled == parent_sampled assert propagation_context.dynamic_sampling_context == { - "trace_id": "566e3688a61d4bc888951642d6f14a19" + "trace_id": "566e3688a61d4bc888951642d6f14a19", + "sample_rand": "0.123456", } diff --git a/tests/test_dsc.py b/tests/test_dsc.py index 18d3792a44..45d3be6897 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -8,7 +8,6 @@ This is not tested in this file. """ -import random from unittest import mock import pytest @@ -176,7 +175,7 @@ def my_traces_sampler(sampling_context): } # We continue the incoming trace and start a new transaction - with mock.patch.object(random, "random", return_value=0.2): + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.125): with sentry_sdk.continue_trace(incoming_http_headers): with sentry_sdk.start_span(name="foo"): pass diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 1015e9f2ad..42f9c1960c 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -1,4 +1,3 @@ -import random from collections import Counter from unittest import mock @@ -70,22 +69,21 @@ def test_root_span_uses_downsample_rate( monitor = sentry_sdk.get_client().monitor monitor.interval = 0.1 - # make sure rng doesn't sample - monkeypatch.setattr(random, "random", lambda: 0.9) - assert monitor.is_healthy() is True monitor.run() assert monitor.is_healthy() is False assert monitor.downsample_factor == 1 - with sentry_sdk.start_span(name="foobar") as root_span: - with sentry_sdk.start_span(name="foospan"): - with sentry_sdk.start_span(name="foospan2"): - with sentry_sdk.start_span(name="foospan3"): - ... + # make sure we don't sample the root span + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.75): + with sentry_sdk.start_span(name="foobar") as root_span: + with sentry_sdk.start_span(name="foospan"): + with sentry_sdk.start_span(name="foospan2"): + with sentry_sdk.start_span(name="foospan3"): + ... - assert root_span.sampled is False - assert root_span.sample_rate == 0.5 + assert root_span.sampled is False + assert root_span.sample_rate == 0.5 assert len(envelopes) == 0 diff --git a/tests/test_propagationcontext.py b/tests/test_propagationcontext.py index 4e1f5567d6..c8749027e4 100644 --- a/tests/test_propagationcontext.py +++ b/tests/test_propagationcontext.py @@ -1,6 +1,19 @@ +from unittest import mock +from unittest.mock import Mock + +import pytest + from sentry_sdk.tracing_utils import Baggage, PropagationContext +SAMPLED_FLAG = { + None: "", + False: "-0", + True: "-1", +} +"""Maps the `sampled` value to the flag appended to the sentry-trace header.""" + + def test_empty_context(): ctx = PropagationContext() @@ -54,6 +67,7 @@ def test_lazy_uuids(): def test_property_setters(): ctx = PropagationContext() + ctx.trace_id = "X234567890abcdef1234567890abcdef" ctx.span_id = "X234567890abcdef" @@ -61,6 +75,7 @@ def test_property_setters(): assert ctx.trace_id == "X234567890abcdef1234567890abcdef" assert ctx._span_id == "X234567890abcdef" assert ctx.span_id == "X234567890abcdef" + assert ctx.dynamic_sampling_context is None def test_update(): @@ -84,3 +99,87 @@ def test_update(): assert ctx.dynamic_sampling_context is None assert not hasattr(ctx, "foo") + + +def test_existing_sample_rand_kept(): + ctx = PropagationContext( + trace_id="00000000000000000000000000000000", + dynamic_sampling_context={"sample_rand": "0.5"}, + ) + + # If sample_rand was regenerated, the value would be 0.919221 based on the trace_id + assert ctx.dynamic_sampling_context["sample_rand"] == "0.5" + + +@pytest.mark.parametrize( + ("parent_sampled", "sample_rate", "expected_interval"), + ( + # Note that parent_sampled and sample_rate do not scale the + # sample_rand value, only determine the range of the value. + # Expected values are determined by parent_sampled, sample_rate, + # and the trace_id. + (None, None, (0.0, 1.0)), + (None, "0.5", (0.0, 1.0)), + (False, None, (0.0, 1.0)), + (True, None, (0.0, 1.0)), + (False, "0.0", (0.0, 1.0)), + (False, "0.01", (0.01, 1.0)), + (True, "0.01", (0.0, 0.01)), + (False, "0.1", (0.1, 1.0)), + (True, "0.1", (0.0, 0.1)), + (False, "0.5", (0.5, 1.0)), + (True, "0.5", (0.0, 0.5)), + (True, "1.0", (0.0, 1.0)), + ), +) +def test_sample_rand_filled(parent_sampled, sample_rate, expected_interval): + """When continuing a trace, we want to fill in the sample_rand value if it's missing.""" + if sample_rate is not None: + sample_rate_str = f",sentry-sample_rate={sample_rate}" # noqa: E231 + else: + sample_rate_str = "" + + # for convenience, we'll just return the lower bound of the interval + mock_uniform = mock.Mock(return_value=expected_interval[0]) + + def mock_random_class(seed): + assert seed == "00000000000000000000000000000000", "seed should be the trace_id" + rv = Mock() + rv.uniform = mock_uniform + return rv + + with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class): + ctx = PropagationContext().from_incoming_data( + { + "sentry-trace": f"00000000000000000000000000000000-0000000000000000{SAMPLED_FLAG[parent_sampled]}", + # Placeholder is needed, since we only add sample_rand if sentry items are present in baggage + "baggage": f"sentry-placeholder=asdf{sample_rate_str}", + } + ) + + assert ( + ctx.dynamic_sampling_context["sample_rand"] + == f"{expected_interval[0]:.6f}" # noqa: E231 + ) + assert mock_uniform.call_count == 1 + assert mock_uniform.call_args[0] == expected_interval + + +def test_sample_rand_rounds_down(): + # Mock value that should round down to 0.999_999 + mock_uniform = mock.Mock(return_value=0.999_999_9) + + def mock_random_class(_): + rv = Mock() + rv.uniform = mock_uniform + return rv + + with mock.patch("sentry_sdk.tracing_utils.Random", mock_random_class): + ctx = PropagationContext().from_incoming_data( + { + "sentry-trace": "00000000000000000000000000000000-0000000000000000", + "baggage": "sentry-placeholder=asdf", + } + ) + + assert ctx.dynamic_sampling_context["sample_rand"] == "0.999999" diff --git a/tests/tracing/test_integration_tests.py b/tests/tracing/test_integration_tests.py index 22deabb692..6fa9d66964 100644 --- a/tests/tracing/test_integration_tests.py +++ b/tests/tracing/test_integration_tests.py @@ -1,7 +1,8 @@ import re -import pytest -import random import sys +from unittest import mock + +import pytest import sentry_sdk from sentry_sdk import ( @@ -141,12 +142,12 @@ def test_dynamic_sampling_head_sdk_creates_dsc( envelopes = capture_envelopes() # make sure transaction is sampled for both cases - monkeypatch.setattr(random, "random", lambda: 0.1) + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", return_value=0.25): - with continue_trace({}): - with start_span(name="Head SDK tx"): - with start_span(op="foo", name="foodesc") as span: - baggage = span.get_baggage() + with continue_trace({}): + with start_span(name="Head SDK tx"): + with start_span(op="foo", name="foodesc") as span: + baggage = span.get_baggage() trace_id = span.trace_id @@ -157,12 +158,14 @@ def test_dynamic_sampling_head_sdk_creates_dsc( "release": "foo", "sample_rate": str(sample_rate), "sampled": "true" if span.sampled else "false", + "sample_rand": "0.250000", "transaction": "Head SDK tx", "trace_id": trace_id, } expected_baggage = ( "sentry-trace_id=%s," + "sentry-sample_rand=0.250000," "sentry-environment=production," "sentry-release=foo," "sentry-transaction=Head%%20SDK%%20tx," @@ -179,6 +182,7 @@ def test_dynamic_sampling_head_sdk_creates_dsc( "release": "foo", "sample_rate": str(sample_rate), "sampled": "true" if span.sampled else "false", + "sample_rand": "0.250000", "transaction": "Head SDK tx", "trace_id": trace_id, } diff --git a/tests/tracing/test_sample_rand.py b/tests/tracing/test_sample_rand.py new file mode 100644 index 0000000000..b8f5c042ed --- /dev/null +++ b/tests/tracing/test_sample_rand.py @@ -0,0 +1,55 @@ +from unittest import mock + +import pytest + +import sentry_sdk +from sentry_sdk.tracing_utils import Baggage + + +@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) +@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +def test_deterministic_sampled(sentry_init, capture_events, sample_rate, sample_rand): + """ + Test that sample_rand is generated on new traces, that it is used to + make the sampling decision, and that it is included in the transaction's + baggage. + """ + sentry_init(traces_sample_rate=sample_rate) + events = capture_events() + + with mock.patch( + "sentry_sdk.tracing_utils.Random.uniform", return_value=sample_rand + ): + with sentry_sdk.start_transaction() as transaction: + assert ( + transaction.get_baggage().sentry_items["sample_rand"] + == f"{sample_rand:.6f}" # noqa: E231 + ) + + # Transaction event captured if sample_rand < sample_rate, indicating that + # sample_rand is used to make the sampling decision. + assert len(events) == int(sample_rand < sample_rate) + + +@pytest.mark.parametrize("sample_rand", (0.0, 0.25, 0.5, 0.75)) +@pytest.mark.parametrize("sample_rate", (0.0, 0.25, 0.5, 0.75, 1.0)) +def test_transaction_uses_incoming_sample_rand( + sentry_init, capture_events, sample_rate, sample_rand +): + """ + Test that the transaction uses the sample_rand value from the incoming baggage. + """ + baggage = Baggage(sentry_items={"sample_rand": f"{sample_rand:.6f}"}) # noqa: E231 + + sentry_init(traces_sample_rate=sample_rate) + events = capture_events() + + with sentry_sdk.start_transaction(baggage=baggage) as transaction: + assert ( + transaction.get_baggage().sentry_items["sample_rand"] + == f"{sample_rand:.6f}" # noqa: E231 + ) + + # Transaction event captured if sample_rand < sample_rate, indicating that + # sample_rand is used to make the sampling decision. + assert len(events) == int(sample_rand < sample_rate) diff --git a/tests/tracing/test_sample_rand_propagation.py b/tests/tracing/test_sample_rand_propagation.py new file mode 100644 index 0000000000..ea3ea548ff --- /dev/null +++ b/tests/tracing/test_sample_rand_propagation.py @@ -0,0 +1,43 @@ +""" +These tests exist to verify that Scope.continue_trace() correctly propagates the +sample_rand value onto the transaction's baggage. + +We check both the case where there is an incoming sample_rand, as well as the case +where we need to compute it because it is missing. +""" + +from unittest import mock +from unittest.mock import Mock + +import sentry_sdk + + +def test_continue_trace_with_sample_rand(): + """ + Test that an incoming sample_rand is propagated onto the transaction's baggage. + """ + headers = { + "sentry-trace": "00000000000000000000000000000000-0000000000000000-0", + "baggage": "sentry-sample_rand=0.1,sentry-sample_rate=0.5", + } + + transaction = sentry_sdk.continue_trace(headers) + assert transaction.get_baggage().sentry_items["sample_rand"] == "0.1" + + +def test_continue_trace_missing_sample_rand(): + """ + Test that a missing sample_rand is filled in onto the transaction's baggage. + """ + + headers = { + "sentry-trace": "00000000000000000000000000000000-0000000000000000", + "baggage": "sentry-placeholder=asdf", + } + + mock_uniform = Mock(return_value=0.5) + + with mock.patch("sentry_sdk.tracing_utils.Random.uniform", mock_uniform): + transaction = sentry_sdk.continue_trace(headers) + + assert transaction.get_baggage().sentry_items["sample_rand"] == "0.500000" diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index 49ddc55c7f..b418e5a572 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -6,6 +6,7 @@ import sentry_sdk from sentry_sdk import start_span, capture_exception +from sentry_sdk.tracing_utils import Baggage from sentry_sdk.utils import logger @@ -58,9 +59,9 @@ def test_uses_traces_sample_rate_correctly( ): sentry_init(traces_sample_rate=traces_sample_rate) - with mock.patch.object(random, "random", return_value=0.5): - transaction = start_span(name="dogpark") - assert transaction.sampled is expected_decision + baggage = Baggage(sentry_items={"sample_rand": "0.500000"}) + root_span = start_span(name="dogpark", baggage=baggage) + assert root_span.sampled is expected_decision @pytest.mark.parametrize( @@ -74,9 +75,9 @@ def test_uses_traces_sampler_return_value_correctly( ): sentry_init(traces_sampler=mock.Mock(return_value=traces_sampler_return_value)) - with mock.patch.object(random, "random", return_value=0.5): - with start_span(name="dogpark") as span: - assert span.sampled is expected_decision + baggage = Baggage(sentry_items={"sample_rand": "0.500000"}) + root_span = start_span(name="dogpark", baggage=baggage) + assert root_span.sampled is expected_decision @pytest.mark.parametrize("traces_sampler_return_value", [True, False])