diff --git a/sentry_sdk/integrations/_wsgi_common.py b/sentry_sdk/integrations/_wsgi_common.py index 072a102b7c..baca0f7034 100644 --- a/sentry_sdk/integrations/_wsgi_common.py +++ b/sentry_sdk/integrations/_wsgi_common.py @@ -1,4 +1,5 @@ import json +from contextlib import contextmanager from copy import deepcopy import sentry_sdk @@ -15,6 +16,7 @@ if TYPE_CHECKING: from typing import Any from typing import Dict + from typing import Iterator from typing import Mapping from typing import MutableMapping from typing import Optional @@ -50,6 +52,13 @@ ) +# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support +@contextmanager +def nullcontext(): + # type: () -> Iterator[None] + yield + + def request_body_within_bounds(client, content_length): # type: (Optional[sentry_sdk.client.BaseClient], int) -> bool if client is None: diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 8426486ce5..22e42acb6d 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -19,6 +19,7 @@ ) from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, + nullcontext, ) from sentry_sdk.sessions import track_session from sentry_sdk.tracing import ( @@ -190,9 +191,10 @@ async def _run_app(self, scope, receive, send, asgi_version): ) method = scope.get("method", "").upper() - if method in self.http_methods_to_capture: - with sentry_sdk.continue_trace(_get_headers(scope)): - with sentry_sdk.start_transaction( + should_trace = method in self.http_methods_to_capture + with sentry_sdk.continue_trace(_get_headers(scope)): + with ( + sentry_sdk.start_transaction( op=( OP.WEBSOCKET_SERVER if ty == "websocket" @@ -202,37 +204,42 @@ async def _run_app(self, scope, receive, send, asgi_version): source=transaction_source, origin=self.span_origin, custom_sampling_context={"asgi_scope": scope}, - ) as transaction: + ) + if should_trace + else nullcontext() + ) as transaction: + if transaction is not None: logger.debug( "[ASGI] Started transaction: %s", transaction ) transaction.set_tag("asgi.type", ty) - try: - - async def _sentry_wrapped_send(event): - # type: (Dict[str, Any]) -> Any - is_http_response = ( - event.get("type") == "http.response.start" - and "status" in event - ) - if is_http_response: - transaction.set_http_status(event["status"]) - - return await send(event) - - if asgi_version == 2: - return await self.app(scope)( - receive, _sentry_wrapped_send - ) - else: - return await self.app( - scope, receive, _sentry_wrapped_send - ) - except Exception as exc: - _capture_exception( - exc, mechanism_type=self.mechanism_type + try: + + async def _sentry_wrapped_send(event): + # type: (Dict[str, Any]) -> Any + is_http_response = ( + event.get("type") == "http.response.start" + and transaction is not None + and "status" in event ) - raise exc from None + if is_http_response: + transaction.set_http_status(event["status"]) + + return await send(event) + + if asgi_version == 2: + return await self.app(scope)( + receive, _sentry_wrapped_send + ) + else: + return await self.app( + scope, receive, _sentry_wrapped_send + ) + except Exception as exc: + _capture_exception( + exc, mechanism_type=self.mechanism_type + ) + raise exc from None finally: _asgi_middleware_applied.set(False) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index c19483bc62..f778d7f177 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -8,6 +8,7 @@ from sentry_sdk.integrations._wsgi_common import ( DEFAULT_HTTP_METHODS_TO_CAPTURE, _filter_headers, + nullcontext, ) from sentry_sdk.sessions import track_session from sentry_sdk.tracing import Transaction, TRANSACTION_SOURCE_ROUTE @@ -106,27 +107,31 @@ def __call__(self, environ, start_response): ) method = environ.get("REQUEST_METHOD", "").upper() - if method in self.http_methods_to_capture: - with sentry_sdk.continue_trace(environ): - with sentry_sdk.start_transaction( + should_trace = method in self.http_methods_to_capture + with sentry_sdk.continue_trace(environ): + with ( + sentry_sdk.start_transaction( environ, op=OP.HTTP_SERVER, name="generic WSGI request", source=TRANSACTION_SOURCE_ROUTE, origin=self.span_origin, custom_sampling_context={"wsgi_environ": environ}, - ) as transaction: - try: - response = self.app( - environ, - partial( - _sentry_start_response, - start_response, - transaction, - ), - ) - except BaseException: - reraise(*_capture_exception()) + ) + if should_trace + else nullcontext() + ) as transaction: + try: + response = self.app( + environ, + partial( + _sentry_start_response, + start_response, + transaction, + ), + ) + except BaseException: + reraise(*_capture_exception()) finally: _wsgi_middleware_applied.set(False) diff --git a/tests/integrations/django/test_basic.py b/tests/integrations/django/test_basic.py index 2089f1e936..f755d8c3dd 100644 --- a/tests/integrations/django/test_basic.py +++ b/tests/integrations/django/test_basic.py @@ -1113,6 +1113,9 @@ def test_csrf(sentry_init, client): assert content == b"ok" +# This test is forked because it doesn't clean up after itself properly and makes +# other tests fail to resolve routes +@pytest.mark.forked @pytest.mark.skipif(DJANGO_VERSION < (2, 0), reason="Requires Django > 2.0") def test_custom_urlconf_middleware( settings, sentry_init, client, capture_events, render_span_tree @@ -1202,14 +1205,19 @@ def test_transaction_http_method_default(sentry_init, client, capture_events): By default OPTIONS and HEAD requests do not create a transaction. """ sentry_init( - integrations=[DjangoIntegration()], + integrations=[ + DjangoIntegration( + middleware_spans=False, + signals_spans=False, + ) + ], traces_sample_rate=1.0, ) events = capture_events() - client.get("/nomessage") - client.options("/nomessage") - client.head("/nomessage") + client.get(reverse("nomessage")) + client.options(reverse("nomessage")) + client.head(reverse("nomessage")) (event,) = events @@ -1225,15 +1233,17 @@ def test_transaction_http_method_custom(sentry_init, client, capture_events): "OPTIONS", "head", ), # capitalization does not matter + middleware_spans=False, + signals_spans=False, ) ], traces_sample_rate=1.0, ) events = capture_events() - client.get("/nomessage") - client.options("/nomessage") - client.head("/nomessage") + client.get(reverse("nomessage")) + client.options(reverse("nomessage")) + client.head(reverse("nomessage")) assert len(events) == 2