diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 46cb04724b..e2e9837307 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -2,6 +2,9 @@ import sentry_sdk from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.data_collection import ( + _apply_data_collection_filtering_to_query_string, +) from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import ( @@ -14,13 +17,16 @@ SENSITIVE_DATA_SUBSTITUTE, capture_internal_exceptions, ensure_integration_enabled, + has_data_collection_enabled, parse_url, ) if TYPE_CHECKING: - from typing import Any + from typing import Any, Optional from sentry_sdk._types import Attributes + from sentry_sdk.client import BaseClient + from sentry_sdk.utils import ParsedUrl try: @@ -45,6 +51,45 @@ def setup_once() -> None: _install_httpx_async_client() +def _get_url_attributes( + client: "BaseClient", parsed_url: "Optional[ParsedUrl]" +) -> "Attributes": + attributes: "Attributes" = {} + if parsed_url is None: + return attributes + + url_full = parsed_url.url + + if has_data_collection_enabled(client.options): + if parsed_url.query: + filtered_query = _apply_data_collection_filtering_to_query_string( + query_string=parsed_url.query, + behaviour=client.options["data_collection"]["url_query_params"], + ) + if filtered_query: + attributes["url.query"] = filtered_query + url_full += "?" + filtered_query + + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full + + elif should_send_default_pii(): + if parsed_url.query: + attributes["url.query"] = parsed_url.query + url_full += "?" + parsed_url.query + + if parsed_url.fragment: + attributes["url.fragment"] = parsed_url.fragment + url_full += "#" + parsed_url.fragment + + attributes["url.full"] = url_full + + return attributes + + def _install_httpx_client() -> None: real_send = Client.send @@ -57,11 +102,15 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": 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: propagate_trace_headers(client, request) return real_send(self, request, **kwargs) + url_attributes = _get_url_attributes(client, parsed_url) + with sentry_sdk.traces.start_span( name="%s %s" % ( @@ -74,20 +123,7 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": "http.request.method": request.method, }, ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - url_full = parsed_url.url - if parsed_url.query: - url_full += "?" + parsed_url.query - if parsed_url.fragment: - url_full += "#" + parsed_url.fragment - - attributes["url.full"] = url_full - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment + attributes: "Attributes" = dict(url_attributes) propagate_trace_headers(client, request) @@ -135,14 +171,23 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": "reason": rv.reason_phrase, } - if parsed_url and (not is_span_streaming_enabled or should_send_default_pii()): - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) + 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", parsed_url.url), + SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), + SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), + } + ) add_http_breadcrumb(rv.status_code, breadcrumb_data) @@ -166,11 +211,15 @@ async def send( 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: propagate_trace_headers(client, request) return await real_send(self, request, **kwargs) + url_attributes = _get_url_attributes(client, parsed_url) + with sentry_sdk.traces.start_span( name="%s %s" % ( @@ -183,20 +232,7 @@ async def send( "http.request.method": request.method, }, ) as streamed_span: - attributes: "Attributes" = {} - - if parsed_url is not None and should_send_default_pii(): - url_full = parsed_url.url - if parsed_url.query: - url_full += "?" + parsed_url.query - if parsed_url.fragment: - url_full += "#" + parsed_url.fragment - - attributes["url.full"] = url_full - if parsed_url.query: - attributes["url.query"] = parsed_url.query - if parsed_url.fragment: - attributes["url.fragment"] = parsed_url.fragment + attributes: "Attributes" = dict(url_attributes) propagate_trace_headers(client, request) @@ -243,14 +279,23 @@ async def send( SPANDATA.HTTP_STATUS_CODE: rv.status_code, "reason": rv.reason_phrase, } - if parsed_url and (not is_span_streaming_enabled or should_send_default_pii()): - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) + 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", parsed_url.url), + SPANDATA.HTTP_QUERY: url_attributes.get("url.query", ""), + SPANDATA.HTTP_FRAGMENT: url_attributes.get("url.fragment", ""), + } + ) add_http_breadcrumb(rv.status_code, breadcrumb_data) diff --git a/tests/integrations/httpx/test_httpx.py b/tests/integrations/httpx/test_httpx.py index d62f5da4d0..aad670de15 100644 --- a/tests/integrations/httpx/test_httpx.py +++ b/tests/integrations/httpx/test_httpx.py @@ -2189,3 +2189,732 @@ async def test_http_url_attributes_no_query_or_fragment_span_streaming_async( assert http_span["attributes"]["url.full"] == "http://example.com/" else: assert "url.full" not in http_span["attributes"] + + +@pytest.mark.parametrize( + "init_kwargs, expected_query", + [ + pytest.param( + {"send_default_pii": True}, + "toy=tennisball&color=red&auth=secret", + id="send_default_pii_true", + ), + pytest.param( + {"send_default_pii": False}, + None, + id="send_default_pii_false", + ), + pytest.param( + {}, + None, + id="defaults", + ), + pytest.param( + {"_experiments": {"data_collection": {}}}, + "toy=tennisball&color=red&auth=%5BFiltered%5D", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "denylist", "terms": ["toy"]} + } + } + }, + "toy=%5BFiltered%5D&color=red&auth=%5BFiltered%5D", + id="data_collection_denylist_custom_terms", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", + id="data_collection_allowlist", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["auth"]} + } + } + }, + "toy=%5BFiltered%5D&color=%5BFiltered%5D&auth=%5BFiltered%5D", + id="data_collection_allowlist_sensitive_term", + ), + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + None, + id="data_collection_off", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + }, + }, + None, + id="data_collection_wins_over_send_default_pii", + ), + ], +) +def test_url_query_data_collection_span_streaming_sync( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_query +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + httpx.Client().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + if expected_query is None: + assert "url.query" not in http_span["attributes"] + else: + assert http_span["attributes"]["url.query"] == expected_query + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "init_kwargs, expected_query", + [ + pytest.param( + {"send_default_pii": True}, + "toy=tennisball&color=red&auth=secret", + id="send_default_pii_true", + ), + pytest.param( + {"send_default_pii": False}, + None, + id="send_default_pii_false", + ), + pytest.param( + {}, + None, + id="defaults", + ), + pytest.param( + {"_experiments": {"data_collection": {}}}, + "toy=tennisball&color=red&auth=%5BFiltered%5D", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "denylist", "terms": ["toy"]} + } + } + }, + "toy=%5BFiltered%5D&color=red&auth=%5BFiltered%5D", + id="data_collection_denylist_custom_terms", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", + id="data_collection_allowlist", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["auth"]} + } + } + }, + "toy=%5BFiltered%5D&color=%5BFiltered%5D&auth=%5BFiltered%5D", + id="data_collection_allowlist_sensitive_term", + ), + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + None, + id="data_collection_off", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + }, + }, + None, + id="data_collection_wins_over_send_default_pii", + ), + ], +) +async def test_url_query_data_collection_span_streaming_async( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_query +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + await httpx.AsyncClient().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + if expected_query is None: + assert "url.query" not in http_span["attributes"] + else: + assert http_span["attributes"]["url.query"] == expected_query + + +@pytest.mark.parametrize( + "init_kwargs, expected_url_full", + [ + pytest.param( + {"_experiments": {"data_collection": {}}}, + "http://example.com/?toy=tennisball&color=red&auth=%5BFiltered%5D#frag", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "http://example.com/?toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D#frag", + id="data_collection_allowlist", + ), + ], +) +def test_url_full_reassembly_span_streaming_sync( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_url_full +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + httpx.Client().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert http_span["attributes"]["url.full"] == expected_url_full + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "init_kwargs, expected_url_full", + [ + pytest.param( + {"_experiments": {"data_collection": {}}}, + "http://example.com/?toy=tennisball&color=red&auth=%5BFiltered%5D#frag", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "http://example.com/?toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D#frag", + id="data_collection_allowlist", + ), + ], +) +async def test_url_full_reassembly_span_streaming_async( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_url_full +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + await httpx.AsyncClient().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert http_span["attributes"]["url.full"] == expected_url_full + + +@pytest.mark.parametrize( + "init_kwargs, expected_url_full", + [ + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + "http://example.com/#frag", + id="data_collection_off", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + }, + }, + "http://example.com/#frag", + id="data_collection_wins_over_send_default_pii", + ), + pytest.param( + {"send_default_pii": False}, + None, + id="send_default_pii_false", + ), + ], +) +def test_url_query_params_off_keeps_bare_url_span_streaming_sync( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_url_full +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + httpx.Client().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert "url.query" not in http_span["attributes"] + + if expected_url_full is None: + assert "url.full" not in http_span["attributes"] + assert "url.fragment" not in http_span["attributes"] + else: + assert http_span["attributes"]["url.full"] == expected_url_full + assert http_span["attributes"]["url.fragment"] == "frag" + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "init_kwargs, expected_url_full", + [ + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + "http://example.com/#frag", + id="data_collection_off", + ), + pytest.param( + { + "send_default_pii": True, + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + }, + }, + "http://example.com/#frag", + id="data_collection_wins_over_send_default_pii", + ), + pytest.param( + {"send_default_pii": False}, + None, + id="send_default_pii_false", + ), + ], +) +async def test_url_query_params_off_keeps_bare_url_span_streaming_async( + sentry_init, capture_items, httpx_mock, init_kwargs, expected_url_full +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + **init_kwargs, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="test"): + await httpx.AsyncClient().get(url) + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert "url.query" not in http_span["attributes"] + + if expected_url_full is None: + assert "url.full" not in http_span["attributes"] + assert "url.fragment" not in http_span["attributes"] + else: + assert http_span["attributes"]["url.full"] == expected_url_full + assert http_span["attributes"]["url.fragment"] == "frag" + + +@pytest.mark.parametrize( + "init_kwargs, expected_url, expected_query, expected_fragment", + [ + pytest.param( + {"_experiments": {"data_collection": {}}}, + "http://example.com/?toy=tennisball&color=red&auth=%5BFiltered%5D#frag", + "toy=tennisball&color=red&auth=%5BFiltered%5D", + "frag", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "http://example.com/?toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D#frag", + "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", + "frag", + id="data_collection_allowlist", + ), + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + "http://example.com/#frag", + "", + "frag", + id="data_collection_off", + ), + pytest.param( + {"send_default_pii": False}, + None, + None, + None, + id="send_default_pii_false", + ), + ], +) +def test_crumb_url_query_data_collection_span_streaming_sync( + sentry_init, + capture_events, + httpx_mock, + init_kwargs, + expected_url, + expected_query, + expected_fragment, +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + trace_lifecycle="stream", + **init_kwargs, + ) + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="segment"): + events = capture_events() + + httpx.Client().get(url) + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + + if expected_url is None: + assert "url" not in crumb["data"] + assert SPANDATA.HTTP_QUERY not in crumb["data"] + assert SPANDATA.HTTP_FRAGMENT not in crumb["data"] + else: + assert crumb["data"]["url"] == expected_url + assert crumb["data"][SPANDATA.HTTP_QUERY] == expected_query + assert crumb["data"][SPANDATA.HTTP_FRAGMENT] == expected_fragment + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "init_kwargs, expected_url, expected_query, expected_fragment", + [ + pytest.param( + {"_experiments": {"data_collection": {}}}, + "http://example.com/?toy=tennisball&color=red&auth=%5BFiltered%5D#frag", + "toy=tennisball&color=red&auth=%5BFiltered%5D", + "frag", + id="data_collection_denylist_default", + ), + pytest.param( + { + "_experiments": { + "data_collection": { + "url_query_params": {"mode": "allowlist", "terms": ["toy"]} + } + } + }, + "http://example.com/?toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D#frag", + "toy=tennisball&color=%5BFiltered%5D&auth=%5BFiltered%5D", + "frag", + id="data_collection_allowlist", + ), + pytest.param( + { + "_experiments": { + "data_collection": {"url_query_params": {"mode": "off"}} + } + }, + "http://example.com/#frag", + "", + "frag", + id="data_collection_off", + ), + pytest.param( + {"send_default_pii": False}, + None, + None, + None, + id="send_default_pii_false", + ), + ], +) +async def test_crumb_url_query_data_collection_span_streaming_async( + sentry_init, + capture_events, + httpx_mock, + init_kwargs, + expected_url, + expected_query, + expected_fragment, +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + trace_lifecycle="stream", + **init_kwargs, + ) + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="segment"): + events = capture_events() + + await httpx.AsyncClient().get(url) + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + + if expected_url is None: + assert "url" not in crumb["data"] + assert SPANDATA.HTTP_QUERY not in crumb["data"] + assert SPANDATA.HTTP_FRAGMENT not in crumb["data"] + else: + assert crumb["data"]["url"] == expected_url + assert crumb["data"][SPANDATA.HTTP_QUERY] == expected_query + 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, httpx_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. + """ + httpx_mock.add_response() + + sentry_init(integrations=[HttpxIntegration()], **init_kwargs) + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + events = capture_events() + + httpx.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, httpx_mock, init_kwargs +): + httpx_mock.add_response() + + sentry_init(integrations=[HttpxIntegration()], **init_kwargs) + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + events = capture_events() + + await httpx.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( + sentry_init, capture_events, capture_items, httpx_mock +): + httpx_mock.add_response() + + sentry_init( + integrations=[HttpxIntegration()], + traces_sample_rate=1.0, + trace_lifecycle="stream", + _experiments={"data_collection": {}}, + ) + + items = capture_items("span") + + url = "http://example.com/?toy=tennisball&color=red&auth=secret#frag" + + with sentry_sdk.traces.start_span(name="segment"): + events = capture_events() + + with mock.patch( + "sentry_sdk.integrations.httpx.parse_url", + side_effect=ValueError, + ): + httpx.Client().get(url) + + capture_message("Testing!") + + (event,) = events + + sentry_sdk.flush() + + http_span = _get_http_client_span(items) + + assert "url.full" not in http_span["attributes"] + assert "url.query" not in http_span["attributes"] + assert "url.fragment" not in http_span["attributes"] + + crumb = event["breadcrumbs"]["values"][0] + + assert "url" not in crumb["data"] + assert SPANDATA.HTTP_QUERY not in crumb["data"] + assert SPANDATA.HTTP_FRAGMENT not in crumb["data"]