From 31486cf2c9f9a7da4a089f64fa20f4eea9037d27 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 10 Aug 2026 10:40:36 +0200 Subject: [PATCH 1/5] ref(pyreqwest): Move crumbs to integration --- sentry_sdk/integrations/pyreqwest.py | 68 ++++++-- sentry_sdk/tracing_utils.py | 5 +- .../integrations/pyreqwest/test_pyreqwest.py | 150 +++++++++++++++++- 3 files changed, 211 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/pyreqwest.py b/sentry_sdk/integrations/pyreqwest.py index d25d03f470..d8a5d636b7 100644 --- a/sentry_sdk/integrations/pyreqwest.py +++ b/sentry_sdk/integrations/pyreqwest.py @@ -9,6 +9,7 @@ from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import BAGGAGE_HEADER_NAME from sentry_sdk.tracing_utils import ( + add_http_breadcrumb, add_http_request_source, add_sentry_baggage_to_headers, has_span_streaming_enabled, @@ -67,15 +68,18 @@ def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> Non original_method = getattr(cls, method_name) def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": - if not getattr(self, "_sentry_instrumented", False): - integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) - if integration is not None: - self.with_middleware(middleware) - try: - self._sentry_instrumented = True - except (TypeError, AttributeError): - # In case the instance itself is immutable or doesn't allow extra attributes - pass + integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) + + if getattr(self, "_sentry_instrumented", False) or integration is None: + return original_method(self, *args, **kwargs) + + self.with_middleware(middleware) + try: + self._sentry_instrumented = True + except (TypeError, AttributeError): + # In case the instance itself is immutable or doesn't allow extra attributes + pass + return original_method(self, *args, **kwargs) setattr(cls, method_name, sentry_patched_method) @@ -156,6 +160,13 @@ async def sentry_async_middleware( if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return await next_handler.run(request) + method = request.method + parsed_url = None + with capture_internal_exceptions(): + # This needs to be done early because the URL is no longer accessible + # after the request has been sent + parsed_url = parse_url(str(request.url), sanitize=False) + with _sentry_pyreqwest_span(request) as span: response = await next_handler.run(request) if isinstance(span, StreamedSpan): @@ -167,6 +178,22 @@ async def sentry_async_middleware( elif span is not None: span.set_http_status(response.status) + breadcrumb_data = { + SPANDATA.HTTP_METHOD: method, + SPANDATA.HTTP_STATUS_CODE: response.status, + } + + if parsed_url and should_send_default_pii(): + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + + add_http_breadcrumb(response.status, breadcrumb_data) + return response @@ -176,6 +203,13 @@ def sentry_sync_middleware( if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return next_handler.run(request) + method = request.method + parsed_url = None + with capture_internal_exceptions(): + # This needs to be done early because the URL is no longer accessible + # after the request has been sent + parsed_url = parse_url(str(request.url), sanitize=False) + with _sentry_pyreqwest_span(request) as span: response = next_handler.run(request) if isinstance(span, StreamedSpan): @@ -187,4 +221,20 @@ def sentry_sync_middleware( elif span is not None: span.set_http_status(response.status) + breadcrumb_data = { + SPANDATA.HTTP_METHOD: method, + SPANDATA.HTTP_STATUS_CODE: response.status, + } + + if parsed_url and should_send_default_pii(): + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + + add_http_breadcrumb(response.status, breadcrumb_data) + return response diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 480d36e2a1..7ec94b504d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -228,7 +228,10 @@ def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") -> def maybe_create_breadcrumbs_from_span( scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" ) -> None: - if span.op == OP.HTTP_CLIENT and span.origin not in ("auto.http.aiohttp",): + if span.op == OP.HTTP_CLIENT and span.origin not in ( + "auto.http.aiohttp", + "auto.http.pyreqwest", + ): level = None status_code = span._data.get(SPANDATA.HTTP_STATUS_CODE) if status_code: diff --git a/tests/integrations/pyreqwest/test_pyreqwest.py b/tests/integrations/pyreqwest/test_pyreqwest.py index 05a96f8731..63553c809d 100644 --- a/tests/integrations/pyreqwest/test_pyreqwest.py +++ b/tests/integrations/pyreqwest/test_pyreqwest.py @@ -11,10 +11,10 @@ from pyreqwest.simple.sync_request import pyreqwest_get as sync_pyreqwest_get import sentry_sdk -from sentry_sdk import start_transaction +from sentry_sdk import capture_message, start_transaction from sentry_sdk.consts import MATCH_ALL, SPANDATA from sentry_sdk.integrations.pyreqwest import PyreqwestIntegration -from tests.conftest import get_free_port +from tests.conftest import ApproxDict, get_free_port class PyreqwestMockHandler(BaseHTTPRequestHandler): @@ -956,3 +956,149 @@ def fake_start_span(*args, **kwargs): assert SPANDATA.CODE_NAMESPACE in data assert SPANDATA.CODE_FILEPATH in data assert SPANDATA.CODE_FUNCTION in data + + +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_crumb_capture( + sentry_init, + capture_events, + server_port, + send_default_pii, + span_streaming, +): + def before_breadcrumb(crumb, hint): + crumb["data"]["extra"] = "foo" + return crumb + + sentry_init( + integrations=[PyreqwestIntegration()], + before_breadcrumb=before_breadcrumb, + send_default_pii=send_default_pii, + trace_lifecycle="stream" if span_streaming else "static", + ) + + url = f"http://localhost:{server_port}/hello?q=test#frag" + + events = capture_events() + + client = SyncClientBuilder().build() + response = client.get(url).build().send() + assert response.status == 200 + + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + assert crumb["type"] == "http" + assert crumb["category"] == "httplib" + + expected = { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: 200, + "extra": "foo", + } + if send_default_pii: + expected["url"] = f"http://localhost:{server_port}/hello" + expected[SPANDATA.HTTP_QUERY] = "q=test" + expected[SPANDATA.HTTP_FRAGMENT] = "frag" + + assert crumb["data"] == ApproxDict(expected) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("send_default_pii", [True, False]) +@pytest.mark.parametrize("span_streaming", [True, False]) +async def test_async_crumb_capture( + sentry_init, + capture_events, + server_port, + send_default_pii, + span_streaming, +): + sentry_init( + integrations=[PyreqwestIntegration()], + send_default_pii=send_default_pii, + trace_lifecycle="stream" if span_streaming else "static", + ) + + url = f"http://localhost:{server_port}/hello?q=test#frag" + + events = capture_events() + + async with ClientBuilder().build() as client: + response = await client.get(url).build().send() + assert response.status == 200 + + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + assert crumb["type"] == "http" + assert crumb["category"] == "httplib" + + expected = { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: 200, + } + if send_default_pii: + expected["url"] = f"http://localhost:{server_port}/hello" + expected[SPANDATA.HTTP_QUERY] = "q=test" + expected[SPANDATA.HTTP_FRAGMENT] = "frag" + + assert crumb["data"] == ApproxDict(expected) + + +@pytest.mark.parametrize( + "status_code,level", + [ + (200, None), + (301, None), + (403, "warning"), + (405, "warning"), + (500, "error"), + ], +) +@pytest.mark.parametrize("span_streaming", [True, False]) +def test_crumb_capture_client_error( + sentry_init, + capture_events, + server_port, + status_code, + level, + span_streaming, +): + sentry_init( + integrations=[PyreqwestIntegration()], + trace_lifecycle="stream" if span_streaming else "static", + ) + + url = f"http://localhost:{server_port}/status/{status_code}" + + events = capture_events() + + client = SyncClientBuilder().build() + response = client.get(url).build().send() + assert response.status == 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 + + assert crumb["data"] == ApproxDict( + { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: status_code, + } + ) From ccf1de940432fe0d767dc14a7993b995a554c163 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 10 Aug 2026 11:06:53 +0200 Subject: [PATCH 2/5] make it work in async --- sentry_sdk/integrations/pyreqwest.py | 22 +++++++++++++++++++--- sentry_sdk/tracing_utils.py | 12 ++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/pyreqwest.py b/sentry_sdk/integrations/pyreqwest.py index d8a5d636b7..5338e7d025 100644 --- a/sentry_sdk/integrations/pyreqwest.py +++ b/sentry_sdk/integrations/pyreqwest.py @@ -1,3 +1,4 @@ +import inspect from contextlib import contextmanager from typing import Any, Generator @@ -67,13 +68,26 @@ def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> Non original_method = getattr(cls, method_name) + is_async = inspect.iscoroutinefunction(middleware) + def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) if getattr(self, "_sentry_instrumented", False) or integration is None: return original_method(self, *args, **kwargs) - self.with_middleware(middleware) + if is_async: + isolation_scope = sentry_sdk.get_isolation_scope() + + async def bound_middleware( + request: "Request", next_handler: "Next" + ) -> "Response": + return await middleware(request, next_handler, isolation_scope) + + self.with_middleware(bound_middleware) + else: + self.with_middleware(middleware) + try: self._sentry_instrumented = True except (TypeError, AttributeError): @@ -155,7 +169,9 @@ def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": async def sentry_async_middleware( - request: "Request", next_handler: "Next" + request: "Request", + next_handler: "Next", + isolation_scope: "sentry_sdk.Scope", ) -> "Response": if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return await next_handler.run(request) @@ -192,7 +208,7 @@ async def sentry_async_middleware( } ) - add_http_breadcrumb(response.status, breadcrumb_data) + add_http_breadcrumb(response.status, breadcrumb_data, isolation_scope) return response diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 7ec94b504d..62151abd18 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -43,6 +43,7 @@ from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union from sentry_sdk._types import Attributes + from sentry_sdk.scope import Scope SENTRY_TRACE_REGEX = re.compile( @@ -210,7 +211,11 @@ def record_sql_queries( yield span -def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") -> None: +def add_http_breadcrumb( + status_code: "Optional[int]", + data: "dict[str, Any]", + scope: "Optional[Scope]" = None, +) -> None: level = None if status_code: if 500 <= status_code <= 599: @@ -222,7 +227,10 @@ def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") -> if level: kwargs["level"] = level - sentry_sdk.add_breadcrumb(**kwargs) + if scope is not None: + scope.add_breadcrumb(**kwargs) + else: + sentry_sdk.add_breadcrumb(**kwargs) def maybe_create_breadcrumbs_from_span( From 0de51ade87645b5af8ca915edf21a4bc7db3d2f9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 10 Aug 2026 11:09:26 +0200 Subject: [PATCH 3/5] fix sphinx --- sentry_sdk/tracing_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 62151abd18..9568bb1ec1 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -43,7 +43,6 @@ from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union from sentry_sdk._types import Attributes - from sentry_sdk.scope import Scope SENTRY_TRACE_REGEX = re.compile( @@ -214,7 +213,7 @@ def record_sql_queries( def add_http_breadcrumb( status_code: "Optional[int]", data: "dict[str, Any]", - scope: "Optional[Scope]" = None, + scope: "Optional[sentry_sdk.Scope]" = None, ) -> None: level = None if status_code: From 1bdf216515c6d5ad0948d6729441f971fb7ef17f Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 10 Aug 2026 11:25:38 +0200 Subject: [PATCH 4/5] gate no response --- sentry_sdk/integrations/pyreqwest.py | 62 +++++++++++++++------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/sentry_sdk/integrations/pyreqwest.py b/sentry_sdk/integrations/pyreqwest.py index 5338e7d025..dafec0131d 100644 --- a/sentry_sdk/integrations/pyreqwest.py +++ b/sentry_sdk/integrations/pyreqwest.py @@ -183,6 +183,7 @@ async def sentry_async_middleware( # after the request has been sent parsed_url = parse_url(str(request.url), sanitize=False) + response = None with _sentry_pyreqwest_span(request) as span: response = await next_handler.run(request) if isinstance(span, StreamedSpan): @@ -194,21 +195,22 @@ async def sentry_async_middleware( elif span is not None: span.set_http_status(response.status) - breadcrumb_data = { - SPANDATA.HTTP_METHOD: method, - SPANDATA.HTTP_STATUS_CODE: response.status, - } - - if parsed_url and should_send_default_pii(): - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) + if response is not None: + breadcrumb_data = { + SPANDATA.HTTP_METHOD: method, + SPANDATA.HTTP_STATUS_CODE: response.status, + } + + if parsed_url and should_send_default_pii(): + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) - add_http_breadcrumb(response.status, breadcrumb_data, isolation_scope) + add_http_breadcrumb(response.status, breadcrumb_data, isolation_scope) return response @@ -226,6 +228,7 @@ def sentry_sync_middleware( # after the request has been sent parsed_url = parse_url(str(request.url), sanitize=False) + response = None with _sentry_pyreqwest_span(request) as span: response = next_handler.run(request) if isinstance(span, StreamedSpan): @@ -237,20 +240,21 @@ def sentry_sync_middleware( elif span is not None: span.set_http_status(response.status) - breadcrumb_data = { - SPANDATA.HTTP_METHOD: method, - SPANDATA.HTTP_STATUS_CODE: response.status, - } - - if parsed_url and should_send_default_pii(): - breadcrumb_data.update( - { - "url": parsed_url.url, - SPANDATA.HTTP_QUERY: parsed_url.query, - SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, - } - ) - - add_http_breadcrumb(response.status, breadcrumb_data) + if response is not None: + breadcrumb_data = { + SPANDATA.HTTP_METHOD: method, + SPANDATA.HTTP_STATUS_CODE: response.status, + } + + if parsed_url and should_send_default_pii(): + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + + add_http_breadcrumb(response.status, breadcrumb_data) return response From 579d2248f3b94f1f2a114d6b3fde356dd685f51b Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Mon, 10 Aug 2026 12:17:47 +0200 Subject: [PATCH 5/5] simplify --- sentry_sdk/integrations/pyreqwest.py | 18 +-- sentry_sdk/tracing_utils.py | 11 +- .../integrations/pyreqwest/test_pyreqwest.py | 122 ++++++++++++++++-- 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/sentry_sdk/integrations/pyreqwest.py b/sentry_sdk/integrations/pyreqwest.py index dafec0131d..098a625d7e 100644 --- a/sentry_sdk/integrations/pyreqwest.py +++ b/sentry_sdk/integrations/pyreqwest.py @@ -1,4 +1,3 @@ -import inspect from contextlib import contextmanager from typing import Any, Generator @@ -68,25 +67,13 @@ def _patch_builder_method(cls: type, method_name: str, middleware: "Any") -> Non original_method = getattr(cls, method_name) - is_async = inspect.iscoroutinefunction(middleware) - def sentry_patched_method(self: "Any", *args: "Any", **kwargs: "Any") -> "Any": integration = sentry_sdk.get_client().get_integration(PyreqwestIntegration) if getattr(self, "_sentry_instrumented", False) or integration is None: return original_method(self, *args, **kwargs) - if is_async: - isolation_scope = sentry_sdk.get_isolation_scope() - - async def bound_middleware( - request: "Request", next_handler: "Next" - ) -> "Response": - return await middleware(request, next_handler, isolation_scope) - - self.with_middleware(bound_middleware) - else: - self.with_middleware(middleware) + self.with_middleware(middleware) try: self._sentry_instrumented = True @@ -171,7 +158,6 @@ def _sentry_pyreqwest_span(request: "Request") -> "Generator[Any, None, None]": async def sentry_async_middleware( request: "Request", next_handler: "Next", - isolation_scope: "sentry_sdk.Scope", ) -> "Response": if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return await next_handler.run(request) @@ -210,7 +196,7 @@ async def sentry_async_middleware( } ) - add_http_breadcrumb(response.status, breadcrumb_data, isolation_scope) + add_http_breadcrumb(response.status, breadcrumb_data) return response diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 9568bb1ec1..7ec94b504d 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -210,11 +210,7 @@ def record_sql_queries( yield span -def add_http_breadcrumb( - status_code: "Optional[int]", - data: "dict[str, Any]", - scope: "Optional[sentry_sdk.Scope]" = None, -) -> None: +def add_http_breadcrumb(status_code: "Optional[int]", data: "dict[str, Any]") -> None: level = None if status_code: if 500 <= status_code <= 599: @@ -226,10 +222,7 @@ def add_http_breadcrumb( if level: kwargs["level"] = level - if scope is not None: - scope.add_breadcrumb(**kwargs) - else: - sentry_sdk.add_breadcrumb(**kwargs) + sentry_sdk.add_breadcrumb(**kwargs) def maybe_create_breadcrumbs_from_span( diff --git a/tests/integrations/pyreqwest/test_pyreqwest.py b/tests/integrations/pyreqwest/test_pyreqwest.py index 63553c809d..b31f717e6d 100644 --- a/tests/integrations/pyreqwest/test_pyreqwest.py +++ b/tests/integrations/pyreqwest/test_pyreqwest.py @@ -1009,27 +1009,31 @@ def before_breadcrumb(crumb, hint): @pytest.mark.asyncio @pytest.mark.parametrize("send_default_pii", [True, False]) -@pytest.mark.parametrize("span_streaming", [True, False]) async def test_async_crumb_capture( sentry_init, capture_events, server_port, send_default_pii, - span_streaming, ): sentry_init( integrations=[PyreqwestIntegration()], send_default_pii=send_default_pii, - trace_lifecycle="stream" if span_streaming else "static", ) url = f"http://localhost:{server_port}/hello?q=test#frag" events = capture_events() - async with ClientBuilder().build() as client: - response = await client.get(url).build().send() - assert response.status == 200 + # Ensure the isolation scope contextvar is set before pyreqwest spawns + # its middleware on a separate asyncio Task. Without this, the child task + # lazily creates its own isolation scope, and breadcrumbs added there + # don't propagate back to this task's context. + sentry_sdk.get_isolation_scope() + + with sentry_sdk.start_transaction(): + async with ClientBuilder().build() as client: + response = await client.get(url).build().send() + assert response.status == 200 capture_message("Testing!") @@ -1051,6 +1055,49 @@ async def test_async_crumb_capture( assert crumb["data"] == ApproxDict(expected) +@pytest.mark.asyncio +@pytest.mark.parametrize("send_default_pii", [True, False]) +async def test_async_crumb_capture_span_streaming( + sentry_init, + capture_events, + server_port, + send_default_pii, +): + sentry_init( + integrations=[PyreqwestIntegration()], + send_default_pii=send_default_pii, + trace_lifecycle="stream", + ) + + url = f"http://localhost:{server_port}/hello?q=test#frag" + + events = capture_events() + + with sentry_sdk.traces.start_span(name="segment"): + async with ClientBuilder().build() as client: + response = await client.get(url).build().send() + assert response.status == 200 + + capture_message("Testing!") + + (event,) = events + + crumb = event["breadcrumbs"]["values"][0] + assert crumb["type"] == "http" + assert crumb["category"] == "httplib" + + expected = { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: 200, + } + if send_default_pii: + expected["url"] = f"http://localhost:{server_port}/hello" + expected[SPANDATA.HTTP_QUERY] = "q=test" + expected[SPANDATA.HTTP_FRAGMENT] = "frag" + + assert crumb["data"] == ApproxDict(expected) + + @pytest.mark.parametrize( "status_code,level", [ @@ -1061,29 +1108,78 @@ async def test_async_crumb_capture( (500, "error"), ], ) -@pytest.mark.parametrize("span_streaming", [True, False]) def test_crumb_capture_client_error( sentry_init, capture_events, server_port, status_code, level, - span_streaming, ): sentry_init( integrations=[PyreqwestIntegration()], - trace_lifecycle="stream" if span_streaming else "static", ) url = f"http://localhost:{server_port}/status/{status_code}" events = capture_events() - client = SyncClientBuilder().build() - response = client.get(url).build().send() - assert response.status == status_code + with sentry_sdk.start_transaction(): + client = SyncClientBuilder().build() + response = client.get(url).build().send() + assert response.status == status_code - capture_message("Testing!") + 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 + + assert crumb["data"] == ApproxDict( + { + SPANDATA.HTTP_METHOD: "GET", + SPANDATA.HTTP_STATUS_CODE: status_code, + } + ) + + +@pytest.mark.parametrize( + "status_code,level", + [ + (200, None), + (301, None), + (403, "warning"), + (405, "warning"), + (500, "error"), + ], +) +def test_crumb_capture_client_error_span_streaming( + sentry_init, + capture_events, + server_port, + status_code, + level, +): + sentry_init( + integrations=[PyreqwestIntegration()], + ) + + url = f"http://localhost:{server_port}/status/{status_code}" + + events = capture_events() + + with sentry_sdk.traces.start_span(name="segment"): + client = SyncClientBuilder().build() + response = client.get(url).build().send() + assert response.status == status_code + + capture_message("Testing!") (event,) = events