diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 748de16657..4bc873380e 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -9,6 +9,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh ### Changed - The SDK now supports Python 3.7 and higher. +- The default of `traces_sample_rate` changed to `0`. Meaning: Incoming traces will be continued by default. For example, if your frontend sends a `sentry-trace/baggage` headers pair, your SDK will create Spans and send them to Sentry. (The default used to be `None` meaning by default no Spans where created, no matter what headers the frontend sent to your project.) See also: https://docs.sentry.io/platforms/python/configuration/options/#traces_sample_rate - `sentry_sdk.start_span` now only takes keyword arguments. - `sentry_sdk.start_transaction`/`sentry_sdk.start_span` no longer takes the following arguments: `span`, `parent_sampled`, `trace_id`, `span_id` or `parent_span_id`. - You can no longer change the sampled status of a span with `span.sampled = False` after starting it. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index e9d7063105..2c164fba3a 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -511,7 +511,7 @@ def __init__( debug=None, # type: Optional[bool] attach_stacktrace=False, # type: bool ca_certs=None, # type: Optional[str] - traces_sample_rate=None, # type: Optional[float] + traces_sample_rate=0, # type: Optional[float] traces_sampler=None, # type: Optional[TracesSampler] profiles_sample_rate=None, # type: Optional[float] profiles_sampler=None, # type: Optional[TracesSampler] diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index 93560421c0..539216e0d6 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -448,7 +448,10 @@ async def hello(request): async def test_trace_from_headers_if_performance_disabled( sentry_init, aiohttp_client, capture_events ): - sentry_init(integrations=[AioHttpIntegration()]) + sentry_init( + integrations=[AioHttpIntegration()], + traces_sample_rate=None, # disable all performance monitoring + ) async def hello(request): capture_message("It's a good day to try dividing by 0") diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index d0ddef8611..6651642436 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -269,7 +269,9 @@ async def test_has_trace_if_performance_disabled( asgi3_app_with_error_and_msg, capture_events, ): - sentry_init() + sentry_init( + traces_sample_rate=None, # disable all performance monitoring + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) with pytest.raises(ZeroDivisionError): @@ -325,7 +327,9 @@ async def test_trace_from_headers_if_performance_disabled( asgi3_app_with_error_and_msg, capture_events, ): - sentry_init() + sentry_init( + traces_sample_rate=None, # disable all performance monitoring + ) app = SentryAsgiMiddleware(asgi3_app_with_error_and_msg) trace_id = "582b43a4192642f0b136d5159a501701" diff --git a/tests/integrations/django/asgi/test_asgi.py b/tests/integrations/django/asgi/test_asgi.py index f083447ed2..e9df117309 100644 --- a/tests/integrations/django/asgi/test_asgi.py +++ b/tests/integrations/django/asgi/test_asgi.py @@ -321,6 +321,7 @@ async def test_has_trace_if_performance_enabled(sentry_init, capture_events): async def test_has_trace_if_performance_disabled(sentry_init, capture_events): sentry_init( integrations=[DjangoIntegration()], + traces_sample_rate=None, # disable all performance monitoring ) events = capture_events() @@ -386,6 +387,7 @@ async def test_trace_from_headers_if_performance_enabled(sentry_init, capture_ev async def test_trace_from_headers_if_performance_disabled(sentry_init, capture_events): sentry_init( integrations=[DjangoIntegration()], + traces_sample_rate=None, # disable all performance monitoring ) events = capture_events() diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 5b75bbb6af..a37576315f 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -241,6 +241,7 @@ def test_trace_from_headers_if_performance_disabled( http_methods_to_capture=("HEAD",), ) ], + traces_sample_rate=None, # disable all performance monitoring ) events = capture_events() diff --git a/tests/integrations/opentelemetry/test_sampler.py b/tests/integrations/opentelemetry/test_sampler.py index 8cccab05be..7198f6b390 100644 --- a/tests/integrations/opentelemetry/test_sampler.py +++ b/tests/integrations/opentelemetry/test_sampler.py @@ -6,14 +6,16 @@ import sentry_sdk +USE_DEFAULT_TRACES_SAMPLE_RATE = -1 + tracer = trace.get_tracer(__name__) @pytest.mark.parametrize( "traces_sample_rate, expected_num_of_envelopes", [ - # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used) - (-1, 0), + # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=0 will be used) + (USE_DEFAULT_TRACES_SAMPLE_RATE, 0), # traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all. (None, 0), # traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace. @@ -29,7 +31,7 @@ def test_sampling_traces_sample_rate_0_or_100( expected_num_of_envelopes, ): kwargs = {} - if traces_sample_rate != -1: + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: kwargs["traces_sample_rate"] = traces_sample_rate sentry_init(**kwargs) @@ -176,8 +178,8 @@ def keep_only_a(sampling_context): @pytest.mark.parametrize( "traces_sample_rate, expected_num_of_envelopes", [ - # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used) - (-1, 0), + # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=0 will be used) + (USE_DEFAULT_TRACES_SAMPLE_RATE, 1), # traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all. (None, 0), # traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace. @@ -193,7 +195,7 @@ def test_sampling_parent_sampled( capture_envelopes, ): kwargs = {} - if traces_sample_rate != -1: + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: kwargs["traces_sample_rate"] = traces_sample_rate sentry_init(**kwargs) @@ -227,9 +229,11 @@ def test_sampling_parent_sampled( @pytest.mark.parametrize( "traces_sample_rate, upstream_sampled, expected_num_of_envelopes", [ - # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used) - (-1, 0, 0), + # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=0 will be used) + (USE_DEFAULT_TRACES_SAMPLE_RATE, 0, 0), + (USE_DEFAULT_TRACES_SAMPLE_RATE, 1, 1), # traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all. + (None, 0, 0), (None, 1, 0), # traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace. (0, 0, 0), @@ -247,7 +251,7 @@ def test_sampling_parent_dropped( capture_envelopes, ): kwargs = {} - if traces_sample_rate != -1: + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: kwargs["traces_sample_rate"] = traces_sample_rate sentry_init(**kwargs) @@ -281,8 +285,8 @@ def test_sampling_parent_dropped( @pytest.mark.parametrize( "traces_sample_rate, expected_num_of_envelopes", [ - # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=None will be used) - (-1, 0), + # special case for testing, do not pass any traces_sample_rate to init() (the default traces_sample_rate=0 will be used) + (USE_DEFAULT_TRACES_SAMPLE_RATE, 0), # traces_sample_rate=None means do not create new traces, and also do not continue incoming traces. So, no envelopes at all. (None, 0), # traces_sample_rate=0 means do not create new traces (0% of the requests), but continue incoming traces. So envelopes will be created only if there is an incoming trace. @@ -298,7 +302,7 @@ def test_sampling_parent_deferred( capture_envelopes, ): kwargs = {} - if traces_sample_rate != -1: + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: kwargs["traces_sample_rate"] = traces_sample_rate sentry_init(**kwargs) diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 76c80f6c6a..149dd1d7d4 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -238,7 +238,9 @@ def dogpark(environ, start_response): capture_message("Attempting to fetch the ball") raise ValueError("Fetch aborted. The ball was not returned.") - sentry_init() + sentry_init( + traces_sample_rate=None, # disable all performance monitoring + ) app = SentryWsgiMiddleware(dogpark) client = Client(app) events = capture_events() @@ -301,7 +303,9 @@ def dogpark(environ, start_response): capture_message("Attempting to fetch the ball") raise ValueError("Fetch aborted. The ball was not returned.") - sentry_init() + sentry_init( + traces_sample_rate=None, # disable all performance monitoring + ) app = SentryWsgiMiddleware(dogpark) client = Client(app) events = capture_events() diff --git a/tests/test_dsc.py b/tests/test_dsc.py index ea3c0b8988..569b7fd3dc 100644 --- a/tests/test_dsc.py +++ b/tests/test_dsc.py @@ -287,8 +287,8 @@ def my_traces_sampler(sampling_context): "local_traces_sampler_result": None, "local_traces_sample_rate": None, }, - None, # expected_sample_rate - "tracing-disabled-no-transactions-should-be-sent", # expected_sampled (traces_sample_rate=None disables all transaction creation) + 1.0, # expected_sample_rate + "true", # expected_sampled ), ( # 6 traces_sampler overrides incoming (traces_sample_rate not set) { diff --git a/tests/tracing/test_sample_rand_propagation.py b/tests/tracing/test_sample_rand_propagation.py index f598b24154..17bf7a6168 100644 --- a/tests/tracing/test_sample_rand_propagation.py +++ b/tests/tracing/test_sample_rand_propagation.py @@ -18,7 +18,7 @@ def test_continue_trace_with_sample_rand(sentry_init): sentry_init() headers = { - "sentry-trace": "00000000000000000000000000000000-0000000000000000-0", + "sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef-0", "baggage": "sentry-sample_rand=0.1,sentry-sample_rate=0.5", } @@ -34,7 +34,7 @@ def test_continue_trace_missing_sample_rand(sentry_init): sentry_init() headers = { - "sentry-trace": "00000000000000000000000000000000-0000000000000000", + "sentry-trace": "771a43a4192642f0b136d5159a501700-1234567890abcdef", "baggage": "sentry-placeholder=asdf", } diff --git a/tests/tracing/test_sampling.py b/tests/tracing/test_sampling.py index 59780729b7..bfd845d26d 100644 --- a/tests/tracing/test_sampling.py +++ b/tests/tracing/test_sampling.py @@ -310,7 +310,10 @@ def test_records_lost_event_only_if_traces_sampler_enabled( sampled_output, expected_record_lost_event_calls, ): - sentry_init(traces_sampler=traces_sampler) + sentry_init( + traces_sample_rate=None, + traces_sampler=traces_sampler, + ) record_lost_event_calls = capture_record_lost_event_calls() with start_span(name="dogpark") as span: diff --git a/tests/tracing/test_trace_propagation.py b/tests/tracing/test_trace_propagation.py new file mode 100644 index 0000000000..cb4c3fc90d --- /dev/null +++ b/tests/tracing/test_trace_propagation.py @@ -0,0 +1,282 @@ +import pytest +import requests +import sentry_sdk +from http.client import HTTPConnection + +USE_DEFAULT_TRACES_SAMPLE_RATE = -1 + +INCOMING_TRACE_ID = "771a43a4192642f0b136d5159a501700" +INCOMING_HEADERS = { + "sentry-trace": f"{INCOMING_TRACE_ID}-1234567890abcdef", + "baggage": ( + f"sentry-trace_id={INCOMING_TRACE_ID}, " + "sentry-public_key=frontendpublickey," + "sentry-sample_rate=0.01337," + "sentry-release=myfrontend," + "sentry-environment=bird," + "sentry-transaction=bar" + ), +} + + +# +# Proper high level testing for trace propagation. +# Testing the matrix of test cases described here: +# https://develop.sentry.dev/sdk/telemetry/traces/trace-propagation-cheat-sheet/ +# + + +@pytest.fixture +def _mock_putheader(monkeypatch): + """ + Mock HTTPConnection.putheader to capture calls to it. + """ + putheader_calls = [] + original_putheader = HTTPConnection.putheader + + def mock_putheader_fn(self, header, value): + putheader_calls.append((header, value)) + return original_putheader(self, header, value) + + monkeypatch.setattr(HTTPConnection, "putheader", mock_putheader_fn) + return putheader_calls + + +@pytest.mark.parametrize( + "traces_sample_rate", + [ + USE_DEFAULT_TRACES_SAMPLE_RATE, + None, + 0, + 1, + ], + ids=[ + "traces_sample_rate=DEFAULT", + "traces_sample_rate=None", + "traces_sample_rate=0", + "traces_sample_rate=1", + ], +) +def test_no_incoming_trace_and_trace_propagation_targets_matching( + sentry_init, capture_events, _mock_putheader, traces_sample_rate +): + init_kwargs = {} + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: + init_kwargs["traces_sample_rate"] = traces_sample_rate + sentry_init(**init_kwargs) + + events = capture_events() + + NO_INCOMING_HEADERS = {} # noqa: N806 + + with sentry_sdk.continue_trace(NO_INCOMING_HEADERS): + with sentry_sdk.start_span(op="test", name="test"): + requests.get("http://example.com") + + # CHECK if performance data (a transaction/span) is sent to Sentry + if traces_sample_rate == 1: + assert len(events) == 1 + else: + assert len(events) == 0 + + outgoing_request_headers = {key: value for key, value in _mock_putheader} + + # CHECK if trace information is added to the outgoing request + assert "sentry-trace" in outgoing_request_headers + assert "baggage" in outgoing_request_headers + + # CHECK if incoming trace is continued + # (no assert necessary, because there is no incoming trace information) + + +@pytest.mark.parametrize( + "traces_sample_rate", + [ + USE_DEFAULT_TRACES_SAMPLE_RATE, + None, + 0, + 1, + ], + ids=[ + "traces_sample_rate=DEFAULT", + "traces_sample_rate=None", + "traces_sample_rate=0", + "traces_sample_rate=1", + ], +) +def test_no_incoming_trace_and_trace_propagation_targets_not_matching( + sentry_init, capture_events, _mock_putheader, traces_sample_rate +): + init_kwargs = { + "trace_propagation_targets": [ + "http://someothersite.com", + ], + } + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: + init_kwargs["traces_sample_rate"] = traces_sample_rate + sentry_init(**init_kwargs) + + events = capture_events() + + NO_INCOMING_HEADERS = {} # noqa: N806 + + with sentry_sdk.continue_trace(NO_INCOMING_HEADERS): + with sentry_sdk.start_span(op="test", name="test"): + requests.get("http://example.com") + + # CHECK if performance data (a transaction/span) is sent to Sentry + if traces_sample_rate == 1: + assert len(events) == 1 + else: + assert len(events) == 0 + + outgoing_request_headers = {key: value for key, value in _mock_putheader} + + # CHECK if trace information is added to the outgoing request + assert "sentry-trace" not in outgoing_request_headers + assert "baggage" not in outgoing_request_headers + + # CHECK if incoming trace is continued + # (no assert necessary, because there is no incoming trace information, and no outgoing trace information either) + + +@pytest.mark.parametrize( + "traces_sample_rate", + [ + USE_DEFAULT_TRACES_SAMPLE_RATE, + None, + 0, + 1, + ], + ids=[ + "traces_sample_rate=DEFAULT", + "traces_sample_rate=None", + "traces_sample_rate=0", + "traces_sample_rate=1", + ], +) +@pytest.mark.parametrize( + "incoming_parent_sampled", + ["deferred", "1", "0"], + ids=[ + "incoming_parent_sampled=DEFERRED", + "incoming_parent_sampled=1", + "incoming_parent_sampled=0", + ], +) +def test_with_incoming_trace_and_trace_propagation_targets_matching( + sentry_init, + capture_events, + _mock_putheader, + incoming_parent_sampled, + traces_sample_rate, +): + init_kwargs = {} + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: + init_kwargs["traces_sample_rate"] = traces_sample_rate + sentry_init(**init_kwargs) + + events = capture_events() + + incoming_headers = INCOMING_HEADERS.copy() + if incoming_parent_sampled != "deferred": + incoming_headers["sentry-trace"] += f"-{incoming_parent_sampled}" + incoming_headers[ + "baggage" + ] += f',sentry-sampled={"true" if incoming_parent_sampled == "1" else "false"}' # noqa: E231 + + with sentry_sdk.continue_trace(incoming_headers): + with sentry_sdk.start_span(op="test", name="test"): + requests.get("http://example.com") + + # CHECK if performance data (a transaction/span) is sent to Sentry + if traces_sample_rate is None or incoming_parent_sampled == "0": + assert len(events) == 0 + else: + if incoming_parent_sampled == "1" or traces_sample_rate == 1: + assert len(events) == 1 + else: + assert len(events) == 0 + + outgoing_request_headers = {key: value for key, value in _mock_putheader} + + # CHECK if trace information is added to the outgoing request + assert "sentry-trace" in outgoing_request_headers + assert "baggage" in outgoing_request_headers + + # CHECK if incoming trace is continued + # Always continue the incoming trace, no matter traces_sample_rate + assert INCOMING_TRACE_ID in outgoing_request_headers["sentry-trace"] + assert INCOMING_TRACE_ID in outgoing_request_headers["baggage"] + + +@pytest.mark.parametrize( + "traces_sample_rate", + [ + USE_DEFAULT_TRACES_SAMPLE_RATE, + None, + 0, + 1, + ], + ids=[ + "traces_sample_rate=DEFAULT", + "traces_sample_rate=None", + "traces_sample_rate=0", + "traces_sample_rate=1", + ], +) +@pytest.mark.parametrize( + "incoming_parent_sampled", + ["deferred", "1", "0"], + ids=[ + "incoming_parent_sampled=DEFERRED", + "incoming_parent_sampled=1", + "incoming_parent_sampled=0", + ], +) +def test_with_incoming_trace_and_trace_propagation_targets_not_matching( + sentry_init, + capture_events, + _mock_putheader, + incoming_parent_sampled, + traces_sample_rate, +): + init_kwargs = { + "trace_propagation_targets": [ + "http://someothersite.com", + ], + } + if traces_sample_rate != USE_DEFAULT_TRACES_SAMPLE_RATE: + init_kwargs["traces_sample_rate"] = traces_sample_rate + sentry_init(**init_kwargs) + + events = capture_events() + + incoming_headers = INCOMING_HEADERS.copy() + if incoming_parent_sampled != "deferred": + incoming_headers["sentry-trace"] += f"-{incoming_parent_sampled}" + incoming_headers[ + "baggage" + ] += f',sentry-sampled={"true" if incoming_parent_sampled == "1" else "false"}' # noqa: E231 + + with sentry_sdk.continue_trace(incoming_headers): + with sentry_sdk.start_span(op="test", name="test"): + requests.get("http://example.com") + + # CHECK if performance data (a transaction/span) is sent to Sentry + if traces_sample_rate is None or incoming_parent_sampled == "0": + assert len(events) == 0 + else: + if incoming_parent_sampled == "1" or traces_sample_rate == 1: + assert len(events) == 1 + else: + assert len(events) == 0 + + outgoing_request_headers = {key: value for key, value in _mock_putheader} + + # CHECK if trace information is added to the outgoing request + assert "sentry-trace" not in outgoing_request_headers + assert "baggage" not in outgoing_request_headers + + # CHECK if incoming trace is continued + # (no assert necessary, because the trace information is not added to the outgoing request (see previous asserts))