diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 858bf273f2..e396a29c4c 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -36,6 +36,7 @@ TransactionSource, ) from sentry_sdk.tracing_utils import ( + add_http_breadcrumb, add_http_request_source, has_span_streaming_enabled, should_propagate_trace, @@ -504,6 +505,26 @@ async def on_request_end( with capture_internal_exceptions(): add_http_request_source(span) + with capture_internal_exceptions(): + parsed_url = parse_url(str(params.url), sanitize=False) + breadcrumb_data = { + SPANDATA.HTTP_METHOD: params.method.upper(), + SPANDATA.HTTP_STATUS_CODE: status, + "reason": params.response.reason, + } + if parsed_url: + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + add_http_breadcrumb( + status, + breadcrumb_data, + ) + trace_config = TraceConfig() trace_config.on_request_start.append(on_request_start) diff --git a/sentry_sdk/integrations/boto3.py b/sentry_sdk/integrations/boto3.py index 69deefc7b7..ade8fc0016 100644 --- a/sentry_sdk/integrations/boto3.py +++ b/sentry_sdk/integrations/boto3.py @@ -7,7 +7,7 @@ from sentry_sdk.scope import should_send_default_pii from sentry_sdk.traces import StreamedSpan from sentry_sdk.tracing import Span -from sentry_sdk.tracing_utils import has_span_streaming_enabled +from sentry_sdk.tracing_utils import add_http_breadcrumb, has_span_streaming_enabled from sentry_sdk.utils import ( capture_internal_exceptions, parse_url, @@ -112,18 +112,32 @@ def _sentry_request_created( # request.context is an open-ended data-structure # where we can add anything useful in request life cycle. request.context["_sentrysdk_span"] = span + request.context["_sentrysdk_breadcrumb_data"] = { + SPANDATA.HTTP_METHOD: request.method, + "url": request.url, + } def _sentry_after_call( context: "Dict[str, Any]", parsed: "Dict[str, Any]", **kwargs: "Any" ) -> None: span: "Optional[Union[Span, StreamedSpan]]" = context.pop("_sentrysdk_span", None) + breadcrumb_data: "Optional[Dict[str, Any]]" = context.pop( + "_sentrysdk_breadcrumb_data", None + ) # Span could be absent if the integration is disabled. if span is None: return span.__exit__(None, None, None) + with capture_internal_exceptions(): + status_code = parsed.get("ResponseMetadata", {}).get("HTTPStatusCode") + data = breadcrumb_data or {} + if status_code is not None: + data[SPANDATA.HTTP_STATUS_CODE] = status_code + add_http_breadcrumb(status_code, data) + body = parsed.get("Body") if not isinstance(body, StreamingBody): return diff --git a/sentry_sdk/integrations/httpx.py b/sentry_sdk/integrations/httpx.py index 333754e209..7c0f06dab0 100644 --- a/sentry_sdk/integrations/httpx.py +++ b/sentry_sdk/integrations/httpx.py @@ -5,6 +5,7 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import ( + add_http_breadcrumb, add_http_request_source, has_span_streaming_enabled, propagate_trace_headers, @@ -128,6 +129,21 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": 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: + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + add_http_breadcrumb(rv.status_code, breadcrumb_data) + return rv Client.send = send # type: ignore @@ -220,6 +236,21 @@ async def send( 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: + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + add_http_breadcrumb(rv.status_code, breadcrumb_data) + return rv AsyncClient.send = send # type: ignore diff --git a/sentry_sdk/integrations/httpx2.py b/sentry_sdk/integrations/httpx2.py index b658ce8ba4..fad72deec9 100644 --- a/sentry_sdk/integrations/httpx2.py +++ b/sentry_sdk/integrations/httpx2.py @@ -5,6 +5,7 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing_utils import ( + add_http_breadcrumb, add_http_request_source, has_span_streaming_enabled, propagate_trace_headers, @@ -129,6 +130,21 @@ def send(self: "Client", request: "Request", **kwargs: "Any") -> "Response": 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: + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + add_http_breadcrumb(rv.status_code, breadcrumb_data) + return rv Client.send = send # type: ignore @@ -222,6 +238,21 @@ async def send( 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: + breadcrumb_data.update( + { + "url": parsed_url.url, + SPANDATA.HTTP_QUERY: parsed_url.query, + SPANDATA.HTTP_FRAGMENT: parsed_url.fragment, + } + ) + add_http_breadcrumb(rv.status_code, breadcrumb_data) + return rv AsyncClient.send = send # type: ignore diff --git a/sentry_sdk/integrations/pyreqwest.py b/sentry_sdk/integrations/pyreqwest.py index d25d03f470..2a81b7afe7 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, @@ -156,6 +157,14 @@ async def sentry_async_middleware( if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return await next_handler.run(request) + method = request.method + + # If we want to access request.url, we need to do it early. It can't be + # retrieved after the request has been sent + parsed_url = None + with capture_internal_exceptions(): + 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 +176,21 @@ 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: + 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 +200,14 @@ def sentry_sync_middleware( if sentry_sdk.get_client().get_integration(PyreqwestIntegration) is None: return next_handler.run(request) + method = request.method + + # If we want to access request.url, we need to do it early. It can't be + # retrieved after the request has been sent + parsed_url = None + with capture_internal_exceptions(): + 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 +219,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: + 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 b3658f6f91..c740397653 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -210,6 +210,22 @@ def record_sql_queries( yield span +def add_http_breadcrumb(status_code, data): + # type: (Optional[int], dict[str, Any]) -> None + level = None + if status_code: + if 500 <= status_code <= 599: + level = "error" + elif 400 <= status_code <= 499: + level = "warning" + + kwargs: "dict[str, Any]" = {"type": "http", "category": "httplib", "data": data} + if level: + kwargs["level"] = level + + sentry_sdk.add_breadcrumb(**kwargs) + + def maybe_create_breadcrumbs_from_span( scope: "sentry_sdk.Scope", span: "sentry_sdk.tracing.Span" ) -> None: