From 41c85ea1ecaf920f168694659fc333b8b57147f5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 3 Sep 2026 12:05:30 +0200 Subject: [PATCH 1/3] chore(httpx2): Remove transaction-based tracing --- sentry_sdk/integrations/httpx2.py | 219 +++++++++--------------------- 1 file changed, 66 insertions(+), 153 deletions(-) diff --git a/sentry_sdk/integrations/httpx2.py b/sentry_sdk/integrations/httpx2.py index 57dbb32bf5..58ef6169de 100644 --- a/sentry_sdk/integrations/httpx2.py +++ b/sentry_sdk/integrations/httpx2.py @@ -8,7 +8,6 @@ add_http_breadcrumb, add_http_request_source, get_url_attributes, - has_span_streaming_enabled, propagate_trace_headers, ) from sentry_sdk.utils import ( @@ -22,8 +21,6 @@ if TYPE_CHECKING: from typing import Any - from sentry_sdk._types import Attributes - try: from httpx2 import AsyncClient, Client, Request, Response @@ -57,80 +54,47 @@ def _install_httpx2_client() -> None: @ensure_integration_enabled(Httpx2Integration, real_send) def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": client = sentry_sdk.get_client() - is_span_streaming_enabled = has_span_streaming_enabled(client.options) parsed_url = None with capture_internal_exceptions(): parsed_url = parse_url(str(request.url), sanitize=False) - url_attributes: "Attributes" = {} - - if is_span_streaming_enabled: - if sentry_sdk.traces.get_current_span() is None: - span_ctx = nullcontext() - else: - span_ctx = sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) - - url_attributes = get_url_attributes(client, parsed_url) - - with span_ctx as streamed_span: - propagate_trace_headers(client, request) - - try: - rv = real_send(self, request, **kwargs) - - if streamed_span is not None: - streamed_span.status = ( - "error" if rv.status_code >= 400 else "ok" - ) - streamed_span.set_attribute( - "http.response.status_code", rv.status_code - ) - finally: - if streamed_span is not None: - streamed_span.set_attributes(url_attributes) - - if streamed_span is not None: - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) + url_attributes = get_url_attributes(client, parsed_url) + + if sentry_sdk.traces.get_current_span() is None: + span_ctx = nullcontext() else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, + span_ctx = sentry_sdk.traces.start_span( name="%s %s" % ( request.method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) - propagate_trace_headers(client, request) + with span_ctx as span: + propagate_trace_headers(client, request) + try: rv = real_send(self, request, **kwargs) - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) + if span is not None: + span.status = "error" if rv.status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", rv.status_code) + finally: + if span is not None: + span.set_attributes(url_attributes) - with capture_internal_exceptions(): - add_http_request_source(span) + if span is not None: + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(span) breadcrumb_data = { SPANDATA.HTTP_METHOD: request.method, @@ -138,23 +102,14 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": "reason": rv.reason_phrase, } - if parsed_url: - if not is_span_streaming_enabled: - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) - elif url_attributes: - breadcrumb_data.update( - { - "url": url_attributes.get("url.full", ""), - SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), - SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), - } - ) + if parsed_url and url_attributes: + breadcrumb_data.update( + { + "url": url_attributes.get("url.full", ""), + SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), + SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), + } + ) add_http_breadcrumb(rv.status_code, breadcrumb_data) @@ -173,102 +128,60 @@ async def send( if client.get_integration(Httpx2Integration) is None: return await real_send(self, request, **kwargs) - is_span_streaming_enabled = has_span_streaming_enabled(client.options) parsed_url = None with capture_internal_exceptions(): parsed_url = parse_url(str(request.url), sanitize=False) - url_attributes: "Attributes" = {} - - if is_span_streaming_enabled: - if sentry_sdk.traces.get_current_span() is None: - span_ctx = nullcontext() - else: - span_ctx = sentry_sdk.traces.start_span( - name="%s %s" - % ( - request.method, - parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, - ), - attributes={ - "sentry.op": OP.HTTP_CLIENT, - "sentry.origin": Httpx2Integration.origin, - "http.request.method": request.method, - }, - ) - - url_attributes = get_url_attributes(client, parsed_url) - - with span_ctx as streamed_span: - propagate_trace_headers(client, request) - - try: - rv = await real_send(self, request, **kwargs) - - if streamed_span is not None: - streamed_span.status = ( - "error" if rv.status_code >= 400 else "ok" - ) - streamed_span.set_attribute( - "http.response.status_code", rv.status_code - ) - finally: - if streamed_span is not None: - streamed_span.set_attributes(url_attributes) - - if streamed_span is not None: - # Needs to happen within the context manager as we want to attach the - # final data before the span finishes and is sent for ingesting. - with capture_internal_exceptions(): - add_http_request_source(streamed_span) + if sentry_sdk.traces.get_current_span() is None: + span_ctx = nullcontext() else: - with sentry_sdk.start_span( - op=OP.HTTP_CLIENT, + span_ctx = sentry_sdk.traces.start_span( name="%s %s" % ( request.method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE, ), - origin=Httpx2Integration.origin, - ) as span: - span.set_data(SPANDATA.HTTP_METHOD, request.method) - if parsed_url is not None: - span.set_data("url", parsed_url.url) - span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query) - span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment) + attributes={ + "sentry.op": OP.HTTP_CLIENT, + "sentry.origin": Httpx2Integration.origin, + "http.request.method": request.method, + }, + ) + + url_attributes = get_url_attributes(client, parsed_url) - propagate_trace_headers(client, request) + with span_ctx as span: + propagate_trace_headers(client, request) + try: rv = await real_send(self, request, **kwargs) - span.set_http_status(rv.status_code) - span.set_data("reason", rv.reason_phrase) + if span is not None: + span.status = "error" if rv.status_code >= 400 else "ok" + span.set_attribute("http.response.status_code", rv.status_code) + finally: + if span is not None: + span.set_attributes(url_attributes) - with capture_internal_exceptions(): - add_http_request_source(span) + if span is not None: + # Needs to happen within the context manager as we want to attach the + # final data before the span finishes and is sent for ingesting. + with capture_internal_exceptions(): + add_http_request_source(span) breadcrumb_data = { SPANDATA.HTTP_METHOD: request.method, SPANDATA.HTTP_STATUS_CODE: rv.status_code, "reason": rv.reason_phrase, } - if parsed_url: - if not is_span_streaming_enabled: - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) - elif url_attributes: - breadcrumb_data.update( - { - "url": url_attributes.get("url.full", ""), - SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), - SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), - } - ) + if parsed_url and url_attributes: + breadcrumb_data.update( + { + "url": url_attributes.get("url.full", ""), + SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), + SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), + } + ) add_http_breadcrumb(rv.status_code, breadcrumb_data) From f25707e94cc5029ec6d918be20ce744119b4bd33 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 3 Sep 2026 12:44:03 +0200 Subject: [PATCH 2/3] . --- tests/integrations/httpx2/test_httpx2.py | 1260 +++++----------------- 1 file changed, 242 insertions(+), 1018 deletions(-) diff --git a/tests/integrations/httpx2/test_httpx2.py b/tests/integrations/httpx2/test_httpx2.py index 76d77ed93f..dce945bbb4 100644 --- a/tests/integrations/httpx2/test_httpx2.py +++ b/tests/integrations/httpx2/test_httpx2.py @@ -5,8 +5,8 @@ import pytest import sentry_sdk -from sentry_sdk import capture_message, start_transaction -from sentry_sdk.consts import MATCH_ALL, OP, SPANDATA +from sentry_sdk import capture_message +from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.httpx2 import Httpx2Integration from tests.conftest import ApproxDict @@ -126,41 +126,6 @@ def before_breadcrumb(crumb, hint): def test_crumb_capture_without_span_sync(sentry_init, capture_events, httpx2_mock): httpx2_mock.add_response() - sentry_init( - integrations=[Httpx2Integration()], - ) - - url = "http://example.com/" - - events = capture_events() - - response = httpx2.Client().get(url) - - assert response.status_code == 200 - capture_message("Testing!") - - (event,) = events - - crumb = event["breadcrumbs"]["values"][0] - assert crumb["type"] == "http" - assert crumb["category"] == "httplib" - assert crumb["data"] == ApproxDict( - { - "url": url, - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_FRAGMENT: "", - SPANDATA.HTTP_QUERY: "", - SPANDATA.HTTP_STATUS_CODE: 200, - "reason": "OK", - } - ) - - -def test_crumb_capture_without_span_sync_span_streaming( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - sentry_init( integrations=[Httpx2Integration()], trace_lifecycle="stream", @@ -197,42 +162,6 @@ async def test_crumb_capture_without_span_async( ): httpx2_mock.add_response() - sentry_init( - integrations=[Httpx2Integration()], - ) - - url = "http://example.com/" - - events = capture_events() - - response = await httpx2.AsyncClient().get(url) - - assert response.status_code == 200 - capture_message("Testing!") - - (event,) = events - - crumb = event["breadcrumbs"]["values"][0] - assert crumb["type"] == "http" - assert crumb["category"] == "httplib" - assert crumb["data"] == ApproxDict( - { - "url": url, - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_FRAGMENT: "", - SPANDATA.HTTP_QUERY: "", - SPANDATA.HTTP_STATUS_CODE: 200, - "reason": "OK", - } - ) - - -@pytest.mark.asyncio -async def test_crumb_capture_without_span_async_span_streaming( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - sentry_init( integrations=[Httpx2Integration()], trace_lifecycle="stream", @@ -321,948 +250,263 @@ def test_crumb_capture_client_error_sync( { SPANDATA.HTTP_METHOD: "GET", SPANDATA.HTTP_STATUS_CODE: status_code, - } - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "status_code,level", - [ - (200, None), - (301, None), - (403, "warning"), - (405, "warning"), - (500, "error"), - ], -) -@pytest.mark.parametrize("send_default_pii", [True, False]) -async def test_crumb_capture_client_error_async( - sentry_init, capture_events, httpx2_mock, status_code, level, send_default_pii -): - httpx2_mock.add_response(status_code=status_code) - - sentry_init( - integrations=[Httpx2Integration()], - trace_lifecycle="stream", - send_default_pii=send_default_pii, - ) - - url = "http://example.com/" - - with sentry_sdk.traces.start_span(name="segment"): - events = capture_events() - - response = await httpx2.AsyncClient().get(url) - - assert response.status_code == status_code - capture_message("Testing!") - - (event,) = events - - crumb = event["breadcrumbs"]["values"][0] - assert crumb["type"] == "http" - assert crumb["category"] == "httplib" - - if level is None: - assert "level" not in crumb - else: - assert crumb["level"] == level - - if send_default_pii: - assert crumb["data"] == ApproxDict( - { - "url": url, - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_FRAGMENT: "", - SPANDATA.HTTP_QUERY: "", - SPANDATA.HTTP_STATUS_CODE: status_code, - } - ) - else: - assert crumb["data"] == ApproxDict( - { - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_STATUS_CODE: status_code, - } - ) - - -def test_outgoing_trace_headers_legacy_sync(sentry_init, httpx2_mock): - httpx2_mock.add_response() - - sentry_init( - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - ) - - url = "http://example.com/" - - with start_transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="01234567890123456789012345678901", - ) as transaction: - response = httpx2.Client().get(url) - - request_span = transaction._span_recorder.spans[-1] - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, - ) - - -@pytest.mark.asyncio -async def test_outgoing_trace_headers_legacy_async(sentry_init, httpx2_mock): - httpx2_mock.add_response() - - sentry_init( - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - ) - - url = "http://example.com/" - - with start_transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="01234567890123456789012345678901", - ) as transaction: - response = await httpx2.AsyncClient().get(url) - - request_span = transaction._span_recorder.spans[-1] - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, - ) - - -def test_outgoing_trace_headers_append_to_baggage_legacy_sync( - sentry_init, - httpx2_mock, -): - httpx2_mock.add_response() - - sentry_init( - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - release="d08ebdb9309e1b004c6f52202de58a09c2268e42", - ) - - url = "http://example.com/" - - # patch random.randrange to return a predictable sample_rand value - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): - with start_transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="01234567890123456789012345678901", - ) as transaction: - response = httpx2.Client().get(url, headers={"baGGage": "custom=data"}) - - request_span = transaction._span_recorder.spans[-1] - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, - ) - assert ( - response.request.headers["baggage"] - == "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" - ) - - -@pytest.mark.asyncio -async def test_outgoing_trace_headers_append_to_baggage_legacy_async( - sentry_init, - httpx2_mock, -): - httpx2_mock.add_response() - - sentry_init( - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - release="d08ebdb9309e1b004c6f52202de58a09c2268e42", - ) - - url = "http://example.com/" - - # patch random.randrange to return a predictable sample_rand value - with mock.patch("sentry_sdk.tracing_utils.Random.randrange", return_value=500000): - with start_transaction( - name="/interactions/other-dogs/new-dog", - op="greeting.sniff", - trace_id="01234567890123456789012345678901", - ) as transaction: - response = await httpx2.AsyncClient().get( - url, headers={"baGGage": "custom=data"} - ) - - request_span = transaction._span_recorder.spans[-1] - assert response.request.headers[ - "sentry-trace" - ] == "{trace_id}-{parent_span_id}-{sampled}".format( - trace_id=transaction.trace_id, - parent_span_id=request_span.span_id, - sampled=1, - ) - assert ( - response.request.headers["baggage"] - == "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" - ) - - -@pytest.mark.parametrize( - "trace_propagation_targets,url,trace_propagated", - [ - [ - None, - "https://example.com/", - False, - ], - [ - [], - "https://example.com/", - False, - ], - [ - [MATCH_ALL], - "https://example.com/", - True, - ], - [ - ["https://example.com/"], - "https://example.com/", - True, - ], - [ - ["https://example.com/"], - "https://example.com", - False, - ], - [ - ["https://example.com"], - "https://example.com", - True, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://example.net", - False, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://good.example.net", - True, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://good.example.net/some/thing", - True, - ], - ], -) -def test_option_trace_propagation_targets_sync( - sentry_init, - httpx2_mock, - trace_propagation_targets, - url, - trace_propagated, -): - httpx2_mock.add_response() - - sentry_init( - release="test", - trace_propagation_targets=trace_propagation_targets, - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - ) - - with sentry_sdk.start_transaction(): - httpx2.Client().get(url) - - request_headers = httpx2_mock.get_request().headers - - if trace_propagated: - assert "sentry-trace" in request_headers - else: - assert "sentry-trace" not in request_headers - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "trace_propagation_targets,url,trace_propagated", - [ - [ - None, - "https://example.com/", - False, - ], - [ - [], - "https://example.com/", - False, - ], - [ - [MATCH_ALL], - "https://example.com/", - True, - ], - [ - ["https://example.com/"], - "https://example.com/", - True, - ], - [ - ["https://example.com/"], - "https://example.com", - False, - ], - [ - ["https://example.com"], - "https://example.com", - True, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://example.net", - False, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://good.example.net", - True, - ], - [ - ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], - "https://good.example.net/some/thing", - True, - ], - ], -) -async def test_option_trace_propagation_targets_async( - sentry_init, - httpx2_mock, - trace_propagation_targets, - url, - trace_propagated, -): - httpx2_mock.add_response() - - sentry_init( - release="test", - trace_propagation_targets=trace_propagation_targets, - traces_sample_rate=1.0, - integrations=[Httpx2Integration()], - ) - - with sentry_sdk.start_transaction(): - await httpx2.AsyncClient().get(url) - - request_headers = httpx2_mock.get_request().headers - - if trace_propagated: - assert "sentry-trace" in request_headers - else: - assert "sentry-trace" not in request_headers - - -def test_do_not_propagate_outside_transaction(sentry_init, httpx2_mock): - httpx2_mock.add_response() - - sentry_init( - traces_sample_rate=1.0, - trace_propagation_targets=[MATCH_ALL], - integrations=[Httpx2Integration()], - ) - - httpx2_client = httpx2.Client() - httpx2_client.get("http://example.com/") - - request_headers = httpx2_mock.get_request().headers - assert "sentry-trace" not in request_headers - - -@pytest.mark.tests_internal_exceptions -def test_omit_url_data_if_parsing_fails(sentry_init, capture_events, httpx2_mock): - httpx2_mock.add_response() - - sentry_init(integrations=[Httpx2Integration()]) - - httpx2_client = httpx2.Client() - url = "http://example.com" - - events = capture_events() - with mock.patch( - "sentry_sdk.integrations.httpx2.parse_url", - side_effect=ValueError, - ): - response = httpx2_client.get(url) - - assert response.status_code == 200 - capture_message("Testing!") - - (event,) = events - assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict( - { - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_STATUS_CODE: 200, - # no url related data - "reason": "OK", - } - ) - - assert "url" not in event["breadcrumbs"]["values"][0]["data"] - assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"] - assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"] - - -def test_request_source_disabled_legacy_sync(sentry_init, capture_events, httpx2_mock): - httpx2_mock.add_response() - sentry_options = { - "integrations": [Httpx2Integration()], - "traces_sample_rate": 1.0, - "enable_http_request_source": False, - "http_request_source_threshold_ms": 0, - } - - sentry_init(**sentry_options) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - httpx2.Client().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data - - -@pytest.mark.asyncio -async def test_request_source_disabled_legacy_async( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - sentry_options = { - "integrations": [Httpx2Integration()], - "traces_sample_rate": 1.0, - "enable_http_request_source": False, - "http_request_source_threshold_ms": 0, - } - - sentry_init(**sentry_options) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data - - -@pytest.mark.parametrize("enable_http_request_source", [None, True]) -def test_request_source_enabled_legacy_sync( - sentry_init, - capture_events, - enable_http_request_source, - httpx2_mock, -): - httpx2_mock.add_response() - sentry_options = { - "integrations": [Httpx2Integration()], - "traces_sample_rate": 1.0, - "http_request_source_threshold_ms": 0, - } - if enable_http_request_source is not None: - sentry_options["enable_http_request_source"] = enable_http_request_source - - sentry_init(**sentry_options) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - httpx2.Client().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - -@pytest.mark.asyncio -@pytest.mark.parametrize("enable_http_request_source", [None, True]) -async def test_request_source_enabled_legacy_async( - sentry_init, - capture_events, - enable_http_request_source, - httpx2_mock, -): - httpx2_mock.add_response() - sentry_options = { - "integrations": [Httpx2Integration()], - "traces_sample_rate": 1.0, - "http_request_source_threshold_ms": 0, - } - if enable_http_request_source is not None: - sentry_options["enable_http_request_source"] = enable_http_request_source - - sentry_init(**sentry_options) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - -def test_request_source_legacy_sync(sentry_init, capture_events, httpx2_mock): - httpx2_mock.add_response() - - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - http_request_source_threshold_ms=0, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - httpx2.Client().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx2.test_httpx2" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/httpx2/test_httpx2.py" - ) - - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path - - assert data.get(SPANDATA.CODE_FUNCTION) == "test_request_source_legacy_sync" - - -@pytest.mark.asyncio -async def test_request_source_legacy_async(sentry_init, capture_events, httpx2_mock): - httpx2_mock.add_response() - - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - http_request_source_threshold_ms=0, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx2.test_httpx2" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/httpx2/test_httpx2.py" - ) - - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path - - assert data.get(SPANDATA.CODE_FUNCTION) == "test_request_source_legacy_async" - - -def test_request_source_with_module_in_search_path_legacy_sync( - sentry_init, capture_events, httpx2_mock -): - """ - Test that request source is relative to the path of the module it ran in - """ - httpx2_mock.add_response() - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - http_request_source_threshold_ms=0, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - from httpx2_helpers.helpers import get_request_with_client - - get_request_with_client(httpx2.Client(), url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "httpx2_helpers.helpers" - assert data.get(SPANDATA.CODE_FILEPATH) == "httpx2_helpers/helpers.py" - - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path - - assert data.get(SPANDATA.CODE_FUNCTION) == "get_request_with_client" - - -@pytest.mark.asyncio -async def test_request_source_with_module_in_search_path_legacy_async( - sentry_init, capture_events, httpx2_mock -): - """ - Test that request source is relative to the path of the module it ran in - """ - httpx2_mock.add_response() - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - http_request_source_threshold_ms=0, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - from httpx2_helpers.helpers import async_get_request_with_client - - await async_get_request_with_client(httpx2.AsyncClient(), url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "httpx2_helpers.helpers" - assert data.get(SPANDATA.CODE_FILEPATH) == "httpx2_helpers/helpers.py" - - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path - - assert data.get(SPANDATA.CODE_FUNCTION) == "async_get_request_with_client" - - -def test_no_request_source_if_duration_too_short_legacy_sync( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - # Threshold so high no real request will ever exceed it - http_request_source_threshold_ms=9999999, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - httpx2.Client().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data - - -@pytest.mark.asyncio -async def test_no_request_source_if_duration_too_short_legacy_async( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - # Threshold so high no real request will ever exceed it - http_request_source_threshold_ms=9999999, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO not in data - assert SPANDATA.CODE_NAMESPACE not in data - assert SPANDATA.CODE_FILEPATH not in data - assert SPANDATA.CODE_FUNCTION not in data - - -def test_request_source_if_duration_over_threshold_legacy_sync( - sentry_init, capture_events, httpx2_mock -): - httpx2_mock.add_response() - - sentry_init( - integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - # Threshold is low so any request will exceed it - http_request_source_threshold_ms=0, - ) - - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): - httpx2.Client().get(url) - - (event,) = events - - span = event["spans"][-1] - assert span["description"].startswith("GET") - - data = span.get("data", {}) - - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data - - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx2.test_httpx2" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/httpx2/test_httpx2.py" - ) - - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path - - assert ( - data.get(SPANDATA.CODE_FUNCTION) - == "test_request_source_if_duration_over_threshold_legacy_sync" - ) + } + ) @pytest.mark.asyncio -async def test_request_source_if_duration_over_threshold_legacy_async( - sentry_init, capture_events, httpx2_mock +@pytest.mark.parametrize( + "status_code,level", + [ + (200, None), + (301, None), + (403, "warning"), + (405, "warning"), + (500, "error"), + ], +) +@pytest.mark.parametrize("send_default_pii", [True, False]) +async def test_crumb_capture_client_error_async( + sentry_init, capture_events, httpx2_mock, status_code, level, send_default_pii ): - httpx2_mock.add_response() + httpx2_mock.add_response(status_code=status_code) sentry_init( integrations=[Httpx2Integration()], - traces_sample_rate=1.0, - enable_http_request_source=True, - # Threshold is low so any request will exceed it - http_request_source_threshold_ms=0, + trace_lifecycle="stream", + send_default_pii=send_default_pii, ) - events = capture_events() - url = "http://example.com/" - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) - - (event,) = events + with sentry_sdk.traces.start_span(name="segment"): + events = capture_events() - span = event["spans"][-1] - assert span["description"].startswith("GET") + response = await httpx2.AsyncClient().get(url) - data = span.get("data", {}) + assert response.status_code == status_code + capture_message("Testing!") - assert SPANDATA.CODE_LINENO in data - assert SPANDATA.CODE_NAMESPACE in data - assert SPANDATA.CODE_FILEPATH in data - assert SPANDATA.CODE_FUNCTION in data + (event,) = events - assert type(data.get(SPANDATA.CODE_LINENO)) == int - assert data.get(SPANDATA.CODE_LINENO) > 0 - assert data.get(SPANDATA.CODE_NAMESPACE) == "tests.integrations.httpx2.test_httpx2" - assert data.get(SPANDATA.CODE_FILEPATH).endswith( - "tests/integrations/httpx2/test_httpx2.py" - ) + crumb = event["breadcrumbs"]["values"][0] + assert crumb["type"] == "http" + assert crumb["category"] == "httplib" - is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep - assert is_relative_path + if level is None: + assert "level" not in crumb + else: + assert crumb["level"] == level - assert ( - data.get(SPANDATA.CODE_FUNCTION) - == "test_request_source_if_duration_over_threshold_legacy_async" - ) + if send_default_pii: + assert crumb["data"] == ApproxDict( + { + "url": url, + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_FRAGMENT: "", + SPANDATA.HTTP_QUERY: "", + SPANDATA.HTTP_STATUS_CODE: status_code, + } + ) + else: + assert crumb["data"] == ApproxDict( + { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: status_code, + } + ) -def test_span_origin_legacy_sync(sentry_init, capture_events, httpx2_mock): +@pytest.mark.parametrize( + "trace_propagation_targets,url,trace_propagated", + [ + [ + None, + "https://example.com/", + False, + ], + [ + [], + "https://example.com/", + False, + ], + [ + [MATCH_ALL], + "https://example.com/", + True, + ], + [ + ["https://example.com/"], + "https://example.com/", + True, + ], + [ + ["https://example.com/"], + "https://example.com", + False, + ], + [ + ["https://example.com"], + "https://example.com", + True, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://example.net", + False, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://good.example.net", + True, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://good.example.net/some/thing", + True, + ], + ], +) +def test_option_trace_propagation_targets_sync( + sentry_init, + httpx2_mock, + trace_propagation_targets, + url, + trace_propagated, +): httpx2_mock.add_response() sentry_init( - integrations=[Httpx2Integration()], + release="test", + trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + integrations=[Httpx2Integration()], ) - events = capture_events() - - url = "http://example.com/" - - with start_transaction(name="test_transaction"): + with sentry_sdk.start_transaction(): httpx2.Client().get(url) - (event,) = events + request_headers = httpx2_mock.get_request().headers - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.http.httpx2" + if trace_propagated: + assert "sentry-trace" in request_headers + else: + assert "sentry-trace" not in request_headers @pytest.mark.asyncio -async def test_span_origin_legacy_async(sentry_init, capture_events, httpx2_mock): +@pytest.mark.parametrize( + "trace_propagation_targets,url,trace_propagated", + [ + [ + None, + "https://example.com/", + False, + ], + [ + [], + "https://example.com/", + False, + ], + [ + [MATCH_ALL], + "https://example.com/", + True, + ], + [ + ["https://example.com/"], + "https://example.com/", + True, + ], + [ + ["https://example.com/"], + "https://example.com", + False, + ], + [ + ["https://example.com"], + "https://example.com", + True, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://example.net", + False, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://good.example.net", + True, + ], + [ + ["https://example.com", r"https?:\/\/[\w\-]+(\.[\w\-]+)+\.net"], + "https://good.example.net/some/thing", + True, + ], + ], +) +async def test_option_trace_propagation_targets_async( + sentry_init, + httpx2_mock, + trace_propagation_targets, + url, + trace_propagated, +): httpx2_mock.add_response() sentry_init( - integrations=[Httpx2Integration()], + release="test", + trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + integrations=[Httpx2Integration()], ) - events = capture_events() + with sentry_sdk.start_transaction(): + await httpx2.AsyncClient().get(url) - url = "http://example.com/" + request_headers = httpx2_mock.get_request().headers - with start_transaction(name="test_transaction"): - await httpx2.AsyncClient().get(url) + if trace_propagated: + assert "sentry-trace" in request_headers + else: + assert "sentry-trace" not in request_headers - (event,) = events - assert event["contexts"]["trace"]["origin"] == "manual" - assert event["spans"][0]["origin"] == "auto.http.httpx2" +@pytest.mark.tests_internal_exceptions +def test_omit_url_data_if_parsing_fails(sentry_init, capture_events, httpx2_mock): + httpx2_mock.add_response() + + sentry_init(integrations=[Httpx2Integration()]) + + httpx2_client = httpx2.Client() + url = "http://example.com" + + events = capture_events() + with mock.patch( + "sentry_sdk.integrations.httpx2.parse_url", + side_effect=ValueError, + ): + response = httpx2_client.get(url) + assert response.status_code == 200 + capture_message("Testing!") -def _get_http_client_span(items): - return next( - item.payload - for item in items - if item.payload.get("attributes", {}).get("sentry.op") == OP.HTTP_CLIENT + (event,) = events + assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict( + { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: 200, + # no url related data + "reason": "OK", + } ) + assert "url" not in event["breadcrumbs"]["values"][0]["data"] + assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"] + assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"] -def test_outgoing_trace_headers_span_streaming_sync( - sentry_init, capture_items, httpx2_mock -): + +def test_outgoing_trace_headers_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1292,9 +536,7 @@ def test_outgoing_trace_headers_span_streaming_sync( @pytest.mark.asyncio -async def test_outgoing_trace_headers_span_streaming_async( - sentry_init, capture_items, httpx2_mock -): +async def test_outgoing_trace_headers_async(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1323,7 +565,7 @@ async def test_outgoing_trace_headers_span_streaming_async( ) -def test_outgoing_trace_headers_append_to_baggage_span_streaming_sync( +def test_outgoing_trace_headers_append_to_baggage_sync( sentry_init, capture_items, httpx2_mock, @@ -1357,7 +599,7 @@ def test_outgoing_trace_headers_append_to_baggage_span_streaming_sync( @pytest.mark.asyncio -async def test_outgoing_trace_headers_append_to_baggage_span_streaming_async( +async def test_outgoing_trace_headers_append_to_baggage_async( sentry_init, capture_items, httpx2_mock, @@ -1392,9 +634,7 @@ async def test_outgoing_trace_headers_append_to_baggage_span_streaming_async( assert "sentry-sampled=true" in baggage -def test_outgoing_trace_headers_span_streaming_no_current_span( - sentry_init, httpx2_mock -): +def test_outgoing_trace_headers_no_current_span(sentry_init, httpx2_mock): """ Even when there is no active span, trace propagation headers should still be attached to outgoing requests when span streaming is enabled. @@ -1437,9 +677,7 @@ def test_outgoing_trace_headers_span_streaming_no_current_span( @pytest.mark.asyncio -async def test_outgoing_trace_headers_span_streaming_no_current_span_async( - sentry_init, httpx2_mock -): +async def test_outgoing_trace_headers_no_current_span_async(sentry_init, httpx2_mock): """ The async client must match the sync client: trace propagation headers are attached to outgoing requests even when there is no active span and span @@ -1476,9 +714,7 @@ async def test_outgoing_trace_headers_span_streaming_no_current_span_async( assert f"sentry-trace_id={trace_id}" in request_headers["baggage"] -def test_request_source_disabled_span_streaming_sync( - sentry_init, capture_items, httpx2_mock -): +def test_request_source_disabled_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1507,9 +743,7 @@ def test_request_source_disabled_span_streaming_sync( @pytest.mark.asyncio -async def test_request_source_disabled_span_streaming_async( - sentry_init, capture_items, httpx2_mock -): +async def test_request_source_disabled_async(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1538,7 +772,7 @@ async def test_request_source_disabled_span_streaming_async( @pytest.mark.parametrize("enable_http_request_source", [None, True]) -def test_request_source_enabled_span_streaming_sync( +def test_request_source_enabled_sync( sentry_init, capture_items, enable_http_request_source, @@ -1576,7 +810,7 @@ def test_request_source_enabled_span_streaming_sync( @pytest.mark.asyncio @pytest.mark.parametrize("enable_http_request_source", [None, True]) -async def test_request_source_enabled_span_streaming_async( +async def test_request_source_enabled_async( sentry_init, capture_items, enable_http_request_source, @@ -1612,7 +846,7 @@ async def test_request_source_enabled_span_streaming_async( assert SPANDATA.CODE_FUNCTION in http_span["attributes"] -def test_request_source_span_streaming_sync(sentry_init, capture_items, httpx2_mock): +def test_request_source_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1659,9 +893,7 @@ def test_request_source_span_streaming_sync(sentry_init, capture_items, httpx2_m @pytest.mark.asyncio -async def test_request_source_span_streaming_async( - sentry_init, capture_items, httpx2_mock -): +async def test_request_source_async(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1707,7 +939,7 @@ async def test_request_source_span_streaming_async( ) -def test_request_source_with_module_in_search_path_span_streaming_sync( +def test_request_source_with_module_in_search_path_sync( sentry_init, capture_items, httpx2_mock ): """ @@ -1753,7 +985,7 @@ def test_request_source_with_module_in_search_path_span_streaming_sync( @pytest.mark.asyncio -async def test_request_source_with_module_in_search_path_span_streaming_async( +async def test_request_source_with_module_in_search_path_async( sentry_init, capture_items, httpx2_mock ): """ @@ -1801,7 +1033,7 @@ async def test_request_source_with_module_in_search_path_span_streaming_async( ) -def test_no_request_source_if_duration_too_short_span_streaming_sync( +def test_no_request_source_if_duration_too_short_sync( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -1833,7 +1065,7 @@ def test_no_request_source_if_duration_too_short_span_streaming_sync( @pytest.mark.asyncio -async def test_no_request_source_if_duration_too_short_span_streaming_async( +async def test_no_request_source_if_duration_too_short_async( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -1864,7 +1096,7 @@ async def test_no_request_source_if_duration_too_short_span_streaming_async( assert SPANDATA.CODE_FUNCTION not in http_span["attributes"] -def test_request_source_if_duration_over_threshold_span_streaming_sync( +def test_request_source_if_duration_over_threshold_sync( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -1909,12 +1141,12 @@ def test_request_source_if_duration_over_threshold_span_streaming_sync( assert ( http_span["attributes"][SPANDATA.CODE_FUNCTION] - == "test_request_source_if_duration_over_threshold_span_streaming_sync" + == "test_request_source_if_duration_over_threshold_sync" ) @pytest.mark.asyncio -async def test_request_source_if_duration_over_threshold_span_streaming_async( +async def test_request_source_if_duration_over_threshold_async( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -1959,11 +1191,11 @@ async def test_request_source_if_duration_over_threshold_span_streaming_async( assert ( http_span["attributes"][SPANDATA.CODE_FUNCTION] - == "test_request_source_if_duration_over_threshold_span_streaming_async" + == "test_request_source_if_duration_over_threshold_async" ) -def test_span_origin_span_streaming_sync(sentry_init, capture_items, httpx2_mock): +def test_span_origin_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -1987,9 +1219,7 @@ def test_span_origin_span_streaming_sync(sentry_init, capture_items, httpx2_mock @pytest.mark.asyncio -async def test_span_origin_span_streaming_async( - sentry_init, capture_items, httpx2_mock -): +async def test_span_origin_async(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -2012,9 +1242,7 @@ async def test_span_origin_span_streaming_async( assert http_span["attributes"]["sentry.origin"] == "auto.http.httpx2" -def test_http_url_attributes_span_streaming_sync( - sentry_init, capture_items, httpx2_mock -): +def test_http_url_attributes_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -2043,9 +1271,7 @@ def test_http_url_attributes_span_streaming_sync( @pytest.mark.asyncio -async def test_http_url_attributes_span_streaming_async( - sentry_init, capture_items, httpx2_mock -): +async def test_http_url_attributes_async(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -2073,7 +1299,7 @@ async def test_http_url_attributes_span_streaming_async( assert http_span["attributes"]["http.response.status_code"] == 200 -def test_http_url_attributes_no_query_or_fragment_span_streaming_sync( +def test_http_url_attributes_no_query_or_fragment_sync( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -2104,7 +1330,7 @@ def test_http_url_attributes_no_query_or_fragment_span_streaming_sync( @pytest.mark.asyncio -async def test_http_url_attributes_no_query_or_fragment_span_streaming_async( +async def test_http_url_attributes_no_query_or_fragment_async( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -2134,9 +1360,7 @@ async def test_http_url_attributes_no_query_or_fragment_span_streaming_async( assert http_span["attributes"]["http.response.status_code"] == 200 -def test_http_url_attributes_pii_disabled_span_streaming_sync( - sentry_init, capture_items, httpx2_mock -): +def test_http_url_attributes_pii_disabled_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() sentry_init( @@ -2164,7 +1388,7 @@ def test_http_url_attributes_pii_disabled_span_streaming_sync( @pytest.mark.asyncio -async def test_http_url_attributes_pii_disabled_span_streaming_async( +async def test_http_url_attributes_pii_disabled_async( sentry_init, capture_items, httpx2_mock ): httpx2_mock.add_response() @@ -2270,7 +1494,7 @@ async def test_http_url_attributes_pii_disabled_span_streaming_async( ), ], ) -def test_url_query_data_collection_span_streaming_sync( +def test_url_query_data_collection_sync( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_query ): httpx2_mock.add_response() @@ -2377,7 +1601,7 @@ def test_url_query_data_collection_span_streaming_sync( ), ], ) -async def test_url_query_data_collection_span_streaming_async( +async def test_url_query_data_collection_async( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_query ): httpx2_mock.add_response() From 343386866498d8dea0480244461b7ef83f1e2e5c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 3 Sep 2026 12:51:22 +0200 Subject: [PATCH 3/3] . --- tests/integrations/httpx2/test_httpx2.py | 158 ++++------------------- 1 file changed, 22 insertions(+), 136 deletions(-) diff --git a/tests/integrations/httpx2/test_httpx2.py b/tests/integrations/httpx2/test_httpx2.py index dce945bbb4..629d3d5139 100644 --- a/tests/integrations/httpx2/test_httpx2.py +++ b/tests/integrations/httpx2/test_httpx2.py @@ -6,11 +6,19 @@ import sentry_sdk from sentry_sdk import capture_message -from sentry_sdk.consts import MATCH_ALL, SPANDATA +from sentry_sdk.consts import MATCH_ALL, OP, SPANDATA from sentry_sdk.integrations.httpx2 import Httpx2Integration from tests.conftest import ApproxDict +def _get_http_client_span(items): + return next( + item.payload + for item in items + if item.payload.get("attributes", {}).get("sentry.op") == OP.HTTP_CLIENT + ) + + @pytest.mark.parametrize("send_default_pii", [True, False]) def test_crumb_capture_and_hint_sync( sentry_init, capture_events, httpx2_mock, send_default_pii @@ -380,10 +388,11 @@ def test_option_trace_propagation_targets_sync( release="test", trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + trace_lifecycle="stream", integrations=[Httpx2Integration()], ) - with sentry_sdk.start_transaction(): + with sentry_sdk.traces.start_span(name="span"): httpx2.Client().get(url) request_headers = httpx2_mock.get_request().headers @@ -458,10 +467,11 @@ async def test_option_trace_propagation_targets_async( release="test", trace_propagation_targets=trace_propagation_targets, traces_sample_rate=1.0, + trace_lifecycle="stream", integrations=[Httpx2Integration()], ) - with sentry_sdk.start_transaction(): + with sentry_sdk.traces.start_span(name="span"): await httpx2.AsyncClient().get(url) request_headers = httpx2_mock.get_request().headers @@ -472,40 +482,6 @@ async def test_option_trace_propagation_targets_async( assert "sentry-trace" not in request_headers -@pytest.mark.tests_internal_exceptions -def test_omit_url_data_if_parsing_fails(sentry_init, capture_events, httpx2_mock): - httpx2_mock.add_response() - - sentry_init(integrations=[Httpx2Integration()]) - - httpx2_client = httpx2.Client() - url = "http://example.com" - - events = capture_events() - with mock.patch( - "sentry_sdk.integrations.httpx2.parse_url", - side_effect=ValueError, - ): - response = httpx2_client.get(url) - - assert response.status_code == 200 - capture_message("Testing!") - - (event,) = events - assert event["breadcrumbs"]["values"][0]["data"] == ApproxDict( - { - SPANDATA.HTTP_METHOD: "GET", - SPANDATA.HTTP_STATUS_CODE: 200, - # no url related data - "reason": "OK", - } - ) - - assert "url" not in event["breadcrumbs"]["values"][0]["data"] - assert SPANDATA.HTTP_FRAGMENT not in event["breadcrumbs"]["values"][0]["data"] - assert SPANDATA.HTTP_QUERY not in event["breadcrumbs"]["values"][0]["data"] - - def test_outgoing_trace_headers_sync(sentry_init, capture_items, httpx2_mock): httpx2_mock.add_response() @@ -886,10 +862,7 @@ def test_request_source_sync(sentry_init, capture_items, httpx2_mock): is_relative_path = http_span["attributes"]["code.file.path"][0] != os.sep assert is_relative_path - assert ( - http_span["attributes"][SPANDATA.CODE_FUNCTION] - == "test_request_source_span_streaming_sync" - ) + assert http_span["attributes"][SPANDATA.CODE_FUNCTION] == "test_request_source_sync" @pytest.mark.asyncio @@ -934,8 +907,7 @@ async def test_request_source_async(sentry_init, capture_items, httpx2_mock): assert is_relative_path assert ( - http_span["attributes"][SPANDATA.CODE_FUNCTION] - == "test_request_source_span_streaming_async" + http_span["attributes"][SPANDATA.CODE_FUNCTION] == "test_request_source_async" ) @@ -1651,7 +1623,7 @@ async def test_url_query_data_collection_async( ), ], ) -def test_url_full_reassembly_span_streaming_sync( +def test_url_full_reassembly_sync( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_url_full ): httpx2_mock.add_response() @@ -1699,7 +1671,7 @@ def test_url_full_reassembly_span_streaming_sync( ), ], ) -async def test_url_full_reassembly_span_streaming_async( +async def test_url_full_reassembly_async( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_url_full ): httpx2_mock.add_response() @@ -1754,7 +1726,7 @@ async def test_url_full_reassembly_span_streaming_async( ), ], ) -def test_url_query_params_off_keeps_bare_url_span_streaming_sync( +def test_url_query_params_off_keeps_bare_url_sync( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_url_full ): httpx2_mock.add_response() @@ -1817,7 +1789,7 @@ def test_url_query_params_off_keeps_bare_url_span_streaming_sync( ), ], ) -async def test_url_query_params_off_keeps_bare_url_span_streaming_async( +async def test_url_query_params_off_keeps_bare_url_async( sentry_init, capture_items, httpx2_mock, init_kwargs, expected_url_full ): httpx2_mock.add_response() @@ -1893,7 +1865,7 @@ async def test_url_query_params_off_keeps_bare_url_span_streaming_async( ), ], ) -def test_crumb_url_query_data_collection_span_streaming_sync( +def test_crumb_url_query_data_collection_sync( sentry_init, capture_events, httpx2_mock, @@ -1976,7 +1948,7 @@ def test_crumb_url_query_data_collection_span_streaming_sync( ), ], ) -async def test_crumb_url_query_data_collection_span_streaming_async( +async def test_crumb_url_query_data_collection_async( sentry_init, capture_events, httpx2_mock, @@ -2015,94 +1987,8 @@ async def test_crumb_url_query_data_collection_span_streaming_async( assert crumb["data"][SPANDATA.HTTP_FRAGMENT] == expected_fragment -@pytest.mark.parametrize( - "init_kwargs", - [ - pytest.param( - {"_experiments": {"data_collection": {}}}, - id="data_collection_denylist_default", - ), - pytest.param( - { - "_experiments": { - "data_collection": {"url_query_params": {"mode": "off"}} - } - }, - id="data_collection_off", - ), - ], -) -def test_crumb_url_query_unfiltered_legacy_sync( - sentry_init, capture_events, httpx2_mock, init_kwargs -): - """ - Behaviour that existed prior to data collection and span streaming. - Remove when we've dropped transaction support and have fully migrated - to span streaming. - """ - httpx2_mock.add_response() - - sentry_init(integrations=[Httpx2Integration()], **init_kwargs) - - url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" - - events = capture_events() - - httpx2.Client().get(url) - capture_message("Testing!") - - (event,) = events - - crumb = event["breadcrumbs"]["values"][0] - - assert crumb["data"]["url"] == "http://example.com/" - assert crumb["data"][SPANDATA.HTTP_QUERY] == "toy=tennisball&color=red&auth=secret" - assert crumb["data"][SPANDATA.HTTP_FRAGMENT] == "frag" - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "init_kwargs", - [ - pytest.param( - {"_experiments": {"data_collection": {}}}, - id="data_collection_denylist_default", - ), - pytest.param( - { - "_experiments": { - "data_collection": {"url_query_params": {"mode": "off"}} - } - }, - id="data_collection_off", - ), - ], -) -async def test_crumb_url_query_unfiltered_legacy_async( - sentry_init, capture_events, httpx2_mock, init_kwargs -): - httpx2_mock.add_response() - - sentry_init(integrations=[Httpx2Integration()], **init_kwargs) - - url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" - - events = capture_events() - - await httpx2.AsyncClient().get(url) - capture_message("Testing!") - - (event,) = events - - crumb = event["breadcrumbs"]["values"][0] - - assert crumb["data"]["url"] == "http://example.com/" - assert crumb["data"][SPANDATA.HTTP_QUERY] == "toy=tennisball&color=red&auth=secret" - assert crumb["data"][SPANDATA.HTTP_FRAGMENT] == "frag" - - @pytest.mark.tests_internal_exceptions -def test_omit_url_data_if_parsing_fails_span_streaming( +def test_omit_url_data_if_parsing_fails( sentry_init, capture_events, capture_items, httpx2_mock ): httpx2_mock.add_response()