From 76a7f75344a010500a12464c1868096b206d992f Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Fri, 26 Jul 2024 15:36:10 -0500 Subject: [PATCH 01/29] feat(integrations): Support Litestar (#2413) Support Litestar through a new LitestarIntegration based on porting the existing StarliteIntegration. `starlite` was renamed `litestar` as part of its move to version 2.0. Closes #2413 --- .../test-integrations-web-frameworks-2.yml | 8 + .../split-tox-gh-actions.py | 1 + sentry_sdk/consts.py | 3 + sentry_sdk/integrations/__init__.py | 1 + sentry_sdk/integrations/litestar.py | 285 +++++++++++++++ setup.py | 1 + tests/integrations/litestar/__init__.py | 3 + tests/integrations/litestar/test_litestar.py | 326 ++++++++++++++++++ tox.ini | 13 + 9 files changed, 641 insertions(+) create mode 100644 sentry_sdk/integrations/litestar.py create mode 100644 tests/integrations/litestar/__init__.py create mode 100644 tests/integrations/litestar/test_litestar.py diff --git a/.github/workflows/test-integrations-web-frameworks-2.yml b/.github/workflows/test-integrations-web-frameworks-2.yml index 37d00f8fbf..c56451b751 100644 --- a/.github/workflows/test-integrations-web-frameworks-2.yml +++ b/.github/workflows/test-integrations-web-frameworks-2.yml @@ -59,6 +59,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh "py${{ matrix.python-version }}-falcon-latest" + - name: Test litestar latest + run: | + set -x # print commands that are executed + ./scripts/runtox.sh "py${{ matrix.python-version }}-litestar-latest" - name: Test pyramid latest run: | set -x # print commands that are executed @@ -137,6 +141,10 @@ jobs: run: | set -x # print commands that are executed ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-falcon" + - name: Test litestar pinned + run: | + set -x # print commands that are executed + ./scripts/runtox.sh --exclude-latest "py${{ matrix.python-version }}-litestar" - name: Test pyramid pinned run: | set -x # print commands that are executed diff --git a/scripts/split-tox-gh-actions/split-tox-gh-actions.py b/scripts/split-tox-gh-actions/split-tox-gh-actions.py index d27ab1d45a..b9f978d850 100755 --- a/scripts/split-tox-gh-actions/split-tox-gh-actions.py +++ b/scripts/split-tox-gh-actions/split-tox-gh-actions.py @@ -115,6 +115,7 @@ "asgi", "bottle", "falcon", + "litestar", "pyramid", "quart", "sanic", diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 9a7823dbfb..d41b8d910c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -439,6 +439,9 @@ class OP: HTTP_CLIENT_STREAM = "http.client.stream" HTTP_SERVER = "http.server" MIDDLEWARE_DJANGO = "middleware.django" + MIDDLEWARE_LITESTAR = "middleware.litestar" + MIDDLEWARE_LITESTAR_RECEIVE = "middleware.litestar.receive" + MIDDLEWARE_LITESTAR_SEND = "middleware.litestar.send" MIDDLEWARE_STARLETTE = "middleware.starlette" MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 3c43ed5472..3e1a5dea95 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -91,6 +91,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "sentry_sdk.integrations.huey.HueyIntegration", "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", "sentry_sdk.integrations.langchain.LangchainIntegration", + "sentry_sdk.integrations.litestar.LitestarIntegration", "sentry_sdk.integrations.loguru.LoguruIntegration", "sentry_sdk.integrations.openai.OpenAIIntegration", "sentry_sdk.integrations.pymongo.PyMongoIntegration", diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py new file mode 100644 index 0000000000..fc5800229d --- /dev/null +++ b/sentry_sdk/integrations/litestar.py @@ -0,0 +1,285 @@ +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.consts import OP +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.scope import Scope as SentryScope, should_send_default_pii +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE +from sentry_sdk.utils import ( + ensure_integration_enabled, + event_from_exception, + transaction_from_function, +) + +try: + from litestar import Request, Litestar # type: ignore + from litestar.handlers.base import BaseRouteHandler # type: ignore + from litestar.middleware import DefineMiddleware # type: ignore + from litestar.routes.http import HTTPRoute # type: ignore + from litestar.data_extractors import ConnectionDataExtractor # type: ignore + from pydantic import BaseModel # type: ignore + + if TYPE_CHECKING: + from typing import Any, Dict, List, Optional, Union + + from litestar.types.asgi_types import ASGIApp # type: ignore + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Scope as LitestarScope, + Send, + WebSocketReceiveMessage, + ) + from litestar.middleware import MiddlewareProtocol + from sentry_sdk._types import Event, Hint +except ImportError: + raise DidNotEnable("Litestar is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic Litestar request" + + +class LitestarIntegration(Integration): + identifier = "litestar" + origin = f"auto.http.{identifier}" + + @staticmethod + def setup_once() -> None: + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + # logs an error for every 500 + ignore_logger("litestar") + + +class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): + def __init__(self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin): + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + span_origin=span_origin, + ) + + +def patch_app_init() -> None: + """ + Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. + See: + - https://docs.litestar.dev/2/usage/applications.html#after-exception + - https://docs.litestar.dev/2/usage/middleware/using-middleware.html + """ + old__init__ = Litestar.__init__ + + def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + SentryLitestarASGIMiddleware.__call__ = SentryLitestarASGIMiddleware._run_asgi3 # type: ignore + middleware = kwargs.pop("middleware", None) or [] + kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Litestar.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + def resolve_middleware_wrapper(self: "Any") -> "List[Middleware]": + return [ + enable_span_for_middleware(middleware) + for middleware in old__resolve_middleware_stack(self) + ] + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if ( + not hasattr(middleware, "__call__") # noqa: B004 + or middleware is SentryLitestarASGIMiddleware + ): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call: "ASGIApp" = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call( + self: "MiddlewareProtocol", + scope: "LitestarScope", + receive: "Receive", + send: "Send", + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_call(self, scope, receive, send) + + middleware_name = self.__class__.__name__ + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR, + description=middleware_name, + origin=LitestarIntegration.origin, + ) as middleware_span: + middleware_span.set_tag("litestar.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_RECEIVE, + description=getattr(receive, "__qualname__", str(receive)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await receive(*args, **kwargs) + + receive_name = getattr(receive, "__name__", str(receive)) + receive_patched = receive_name == "_sentry_receive" + new_receive = _sentry_receive if not receive_patched else receive + + # Creating spans for the "send" callback + async def _sentry_send(message: "Message") -> None: + with sentry_sdk.start_span( + op=OP.MIDDLEWARE_LITESTAR_SEND, + description=getattr(send, "__qualname__", str(send)), + origin=LitestarIntegration.origin, + ) as span: + span.set_tag("litestar.middleware_name", middleware_name) + return await send(message) + + send_name = getattr(send, "__name__", str(send)) + send_patched = send_name == "_sentry_send" + new_send = _sentry_send if not send_patched else send + + return await old_call(self, scope, new_receive, new_send) + + not_yet_patched = old_call.__name__ not in ["_create_span_call"] + + if not_yet_patched: + if isinstance(middleware, DefineMiddleware): + middleware.middleware.__call__ = _create_span_call + else: + middleware.__call__ = _create_span_call + + return middleware + + +def patch_http_route_handle() -> None: + old_handle = HTTPRoute.handle + + async def handle_wrapper( + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await old_handle(self, scope, receive, send) + + sentry_scope = SentryScope.get_isolation_scope() + request: "Request[Any, Any]" = scope["app"].request_class( + scope=scope, receive=receive, send=send + ) + extracted_request_data = ConnectionDataExtractor( + parse_body=True, parse_query=True + )(request) + body = extracted_request_data.pop("body") + + request_data = await body + + def event_processor(event: "Event", _: "Hint") -> "Event": + route_handler = scope.get("route_handler") + + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) + if should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + func = None + if route_handler.name is not None: + tx_name = route_handler.name + else: + func = route_handler.fn + if func is not None: + tx_name = transaction_from_function(func) + + tx_info = {"source": SOURCE_FOR_STYLE["endpoint"]} + + if not tx_name: + tx_name = _DEFAULT_TRANSACTION_NAME + tx_info = {"source": TRANSACTION_SOURCE_ROUTE} + + event.update( + { + "request": request_info, + "transaction": tx_name, + "transaction_info": tx_info, + } + ) + return event + + sentry_scope._name = LitestarIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + return await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[Dict[str, Any]]": + scope_user = scope.get("user", {}) + if not scope_user: + return None + if isinstance(scope_user, dict): + return scope_user + if isinstance(scope_user, BaseModel): + return scope_user.dict() + if hasattr(scope_user, "asdict"): # dataclasses + return scope_user.asdict() + + # NOTE: The following section was not ported from the original StarliteIntegration based on starlite 1.x + # because as get_plugin_for_value from starlite 1.x no longer exists in litestar 2.y, and it is not clear + # what would need to be done here + # + # plugin = get_plugin_for_value(scope_user) + # if plugin and not is_async_callable(plugin.to_dict): + # return plugin.to_dict(scope_user) + + return None + + +@ensure_integration_enabled(LitestarIntegration) +def exception_handler(exc: Exception, scope: "LitestarScope") -> None: + user_info: "Optional[Dict[str, Any]]" = None + if should_send_default_pii(): + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + sentry_scope = SentryScope.get_isolation_scope() + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=sentry_sdk.get_client().options, + mechanism={"type": LitestarIntegration.identifier, "handled": False}, + ) + + sentry_sdk.capture_event(event, hint=hint) diff --git a/setup.py b/setup.py index 0cea2dd51d..7b955c4cb2 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ def get_file_text(file_name): "huey": ["huey>=2"], "huggingface_hub": ["huggingface_hub>=0.22"], "langchain": ["langchain>=0.0.210"], + "litestar": ["litestar>=2.0.0"], "loguru": ["loguru>=0.5"], "openai": ["openai>=1.0.0", "tiktoken>=0.3.0"], "opentelemetry": ["opentelemetry-distro>=0.35b0"], diff --git a/tests/integrations/litestar/__init__.py b/tests/integrations/litestar/__init__.py new file mode 100644 index 0000000000..3a4a6235de --- /dev/null +++ b/tests/integrations/litestar/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("litestar") diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py new file mode 100644 index 0000000000..99b0990d7d --- /dev/null +++ b/tests/integrations/litestar/test_litestar.py @@ -0,0 +1,326 @@ +import functools + +import pytest + +from sentry_sdk import capture_message +from sentry_sdk.integrations.litestar import LitestarIntegration + +from typing import Any, Dict + +from litestar import Litestar, get, Controller +from litestar.logging.config import LoggingConfig +from litestar.middleware import AbstractMiddleware +from litestar.middleware.logging import LoggingMiddlewareConfig +from litestar.middleware.rate_limit import RateLimitConfig +from litestar.middleware.session.server_side import ServerSideSessionConfig +from litestar.testing import TestClient + + +class SampleMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send) -> None: + async def do_stuff(message): + if message["type"] == "http.response.start": + # do something here. + pass + await send(message) + + await self.app(scope, receive, do_stuff) + + +class SampleReceiveSendMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send): + message = await receive() + assert message + assert message["type"] == "http.request" + + send_output = await send({"type": "something-unimportant"}) + assert send_output is None + + await self.app(scope, receive, send) + + +class SamplePartialReceiveSendMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send): + message = await receive() + assert message + assert message["type"] == "http.request" + + send_output = await send({"type": "something-unimportant"}) + assert send_output is None + + async def my_receive(*args, **kwargs): + pass + + async def my_send(*args, **kwargs): + pass + + partial_receive = functools.partial(my_receive) + partial_send = functools.partial(my_send) + + await self.app(scope, partial_receive, partial_send) + + +def litestar_app_factory(middleware=None, debug=True, exception_handlers=None): + class MyController(Controller): + path = "/controller" + + @get("/error") + async def controller_error(self) -> None: + raise Exception("Whoa") + + @get("/some_url") + async def homepage_handler() -> Dict[str, Any]: + 1 / 0 + return {"status": "ok"} + + @get("/custom_error", name="custom_name") + async def custom_error() -> Any: + raise Exception("Too Hot") + + @get("/message") + async def message() -> Dict[str, Any]: + capture_message("hi") + return {"status": "ok"} + + @get("/message/{message_id:str}") + async def message_with_id() -> Dict[str, Any]: + capture_message("hi") + return {"status": "ok"} + + logging_config = LoggingConfig() + + app = Litestar( + route_handlers=[ + homepage_handler, + custom_error, + message, + message_with_id, + MyController, + ], + debug=debug, + middleware=middleware, + logging_config=logging_config, + exception_handlers=exception_handlers, + ) + + return app + + +@pytest.mark.parametrize( + "test_url,expected_error,expected_message,expected_tx_name", + [ + ( + "/some_url", + ZeroDivisionError, + "division by zero", + "tests.integrations.litestar.test_litestar.litestar_app_factory..homepage_handler", + ), + ( + "/custom_error", + Exception, + "Too Hot", + "custom_name", + ), + ( + "/controller/error", + Exception, + "Whoa", + "tests.integrations.litestar.test_litestar.litestar_app_factory..MyController.controller_error", + ), + ], +) +def test_catch_exceptions( + sentry_init, + capture_exceptions, + capture_events, + test_url, + expected_error, + expected_message, + expected_tx_name, +): + sentry_init(integrations=[LitestarIntegration()]) + litestar_app = litestar_app_factory() + exceptions = capture_exceptions() + events = capture_events() + + client = TestClient(litestar_app) + try: + client.get(test_url) + except Exception: + pass + + (exc,) = exceptions + assert isinstance(exc, expected_error) + assert str(exc) == expected_message + + (event,) = events + assert event["transaction"] == expected_tx_name + assert event["exception"]["values"][0]["mechanism"]["type"] == "litestar" + + +def test_middleware_spans(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[LitestarIntegration()], + ) + + logging_config = LoggingMiddlewareConfig() + session_config = ServerSideSessionConfig() + rate_limit_config = RateLimitConfig(rate_limit=("hour", 5)) + + litestar_app = litestar_app_factory( + middleware=[ + session_config.middleware, + logging_config.middleware, + rate_limit_config.middleware, + ] + ) + events = capture_events() + + client = TestClient( + litestar_app, raise_server_exceptions=False, base_url="http://testserver.local" + ) + try: + client.get("/message") + except Exception: + pass + + (_, transaction_event) = events + + expected = ["SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"] + + idx = 0 + for span in transaction_event["spans"]: + if span["op"] == "middleware.litestar": + assert span["description"] == expected[idx] + assert span["tags"]["litestar.middleware_name"] == expected[idx] + idx += 1 + + +def test_middleware_callback_spans(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[LitestarIntegration()], + ) + litestar_app = litestar_app_factory(middleware=[SampleMiddleware]) + events = capture_events() + + client = TestClient(litestar_app, raise_server_exceptions=False) + try: + client.get("/message") + except Exception: + pass + + (_, transaction_event) = events + + expected = [ + { + "op": "middleware.litestar", + "description": "SampleMiddleware", + "tags": {"litestar.middleware_name": "SampleMiddleware"}, + }, + { + "op": "middleware.litestar.send", + "description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send", + "tags": {"litestar.middleware_name": "SampleMiddleware"}, + }, + { + "op": "middleware.litestar.send", + "description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send", + "tags": {"litestar.middleware_name": "SampleMiddleware"}, + }, + ] + for idx, span in enumerate(transaction_event["spans"]): + assert span["op"] == expected[idx]["op"] + assert span["description"] == expected[idx]["description"] + assert span["tags"] == expected[idx]["tags"] + + +def test_middleware_receive_send(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[LitestarIntegration()], + ) + litestar_app = litestar_app_factory(middleware=[SampleReceiveSendMiddleware]) + + client = TestClient(litestar_app, raise_server_exceptions=False) + try: + # NOTE: the assert statements checking + # for correct behaviour are in `SampleReceiveSendMiddleware`! + client.get("/message") + except Exception: + pass + + +def test_middleware_partial_receive_send(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[LitestarIntegration()], + ) + litestar_app = litestar_app_factory(middleware=[SamplePartialReceiveSendMiddleware]) + events = capture_events() + + client = TestClient(litestar_app, raise_server_exceptions=False) + try: + client.get("/message") + except Exception: + pass + + (_, transaction_event) = events + + expected = [ + { + "op": "middleware.litestar", + "description": "SamplePartialReceiveSendMiddleware", + "tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + { + "op": "middleware.litestar.receive", + "description": "TestClientTransport.create_receive..receive", + "tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + { + "op": "middleware.litestar.send", + "description": "SentryAsgiMiddleware._run_app.._sentry_wrapped_send", + "tags": {"litestar.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + ] + + for idx, span in enumerate(transaction_event["spans"]): + assert span["op"] == expected[idx]["op"] + assert span["description"].startswith(expected[idx]["description"]) + assert span["tags"] == expected[idx]["tags"] + + +def test_span_origin(sentry_init, capture_events): + sentry_init( + integrations=[LitestarIntegration()], + traces_sample_rate=1.0, + ) + + logging_config = LoggingMiddlewareConfig() + session_config = ServerSideSessionConfig() + rate_limit_config = RateLimitConfig(rate_limit=("hour", 5)) + + litestar_app = litestar_app_factory( + middleware=[ + session_config.middleware, + logging_config.middleware, + rate_limit_config.middleware, + ] + ) + events = capture_events() + + client = TestClient( + litestar_app, raise_server_exceptions=False, base_url="http://testserver.local" + ) + try: + client.get("/message") + except Exception: + pass + + (_, event) = events + + assert event["contexts"]["trace"]["origin"] == "auto.http.litestar" + for span in event["spans"]: + assert span["origin"] == "auto.http.litestar" diff --git a/tox.ini b/tox.ini index 3ab1bae529..67dcd4a6cd 100644 --- a/tox.ini +++ b/tox.ini @@ -159,6 +159,10 @@ envlist = {py3.9,py3.11,py3.12}-langchain-latest {py3.9,py3.11,py3.12}-langchain-notiktoken + # Litestar + {py3.8,py3.11,py3.12}-litestar-v{2.0} + {py3.8,py3.11,py3.12}-litestar-latest + # Loguru {py3.6,py3.11,py3.12}-loguru-v{0.5} {py3.6,py3.11,py3.12}-loguru-latest @@ -489,6 +493,14 @@ deps = langchain-notiktoken: langchain-openai langchain-notiktoken: openai>=1.6.1 + # Litestar + litestar: pytest-asyncio + litestar: python-multipart + litestar: requests + litestar: cryptography + litestar: pydantic>=2.0.0 + litestar-latest: litestar + # Loguru loguru-v0.5: loguru~=0.5.0 loguru-latest: loguru @@ -678,6 +690,7 @@ setenv = huey: TESTPATH=tests/integrations/huey huggingface_hub: TESTPATH=tests/integrations/huggingface_hub langchain: TESTPATH=tests/integrations/langchain + litestar: TESTPATH=tests/integrations/litestar loguru: TESTPATH=tests/integrations/loguru openai: TESTPATH=tests/integrations/openai opentelemetry: TESTPATH=tests/integrations/opentelemetry From 05a19dc79e060bfa8a9a7988eeac91be4b473b78 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Fri, 26 Jul 2024 16:11:37 -0500 Subject: [PATCH 02/29] Remove references to `pydantic` for `LitestarIntegration` `pydantic` is not strictly required for `litestar` as it was for `starlite`. It seems preferable to have an initial implementation excluding any use of `pydantic` and only add handling for anything `pydantic` related if/when the need arises. --- sentry_sdk/integrations/litestar.py | 3 --- tox.ini | 1 - 2 files changed, 4 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index fc5800229d..32c9067b41 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -19,7 +19,6 @@ from litestar.middleware import DefineMiddleware # type: ignore from litestar.routes.http import HTTPRoute # type: ignore from litestar.data_extractors import ConnectionDataExtractor # type: ignore - from pydantic import BaseModel # type: ignore if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Union @@ -251,8 +250,6 @@ def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[Dict[str, Any] return None if isinstance(scope_user, dict): return scope_user - if isinstance(scope_user, BaseModel): - return scope_user.dict() if hasattr(scope_user, "asdict"): # dataclasses return scope_user.asdict() diff --git a/tox.ini b/tox.ini index 67dcd4a6cd..414368f4ff 100644 --- a/tox.ini +++ b/tox.ini @@ -498,7 +498,6 @@ deps = litestar: python-multipart litestar: requests litestar: cryptography - litestar: pydantic>=2.0.0 litestar-latest: litestar # Loguru From a26eb6e69eef1ba0af93c4b59914691101f73d7a Mon Sep 17 00:00:00 2001 From: Kelly Walker Date: Mon, 29 Jul 2024 09:26:06 -0500 Subject: [PATCH 03/29] Update tox.ini Co-authored-by: Ivana Kellyer --- tox.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tox.ini b/tox.ini index 414368f4ff..6e066d7b24 100644 --- a/tox.ini +++ b/tox.ini @@ -498,6 +498,8 @@ deps = litestar: python-multipart litestar: requests litestar: cryptography + litestar-v2.0: litestar~=2.0.0 + litestar-v2.5: litestar~=2.5.0 litestar-latest: litestar # Loguru From 2d3c0672fe9345ed461e71149d310270a7fba931 Mon Sep 17 00:00:00 2001 From: Kelly Walker Date: Mon, 29 Jul 2024 09:26:18 -0500 Subject: [PATCH 04/29] Update tox.ini Co-authored-by: Ivana Kellyer --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6e066d7b24..691eb01662 100644 --- a/tox.ini +++ b/tox.ini @@ -161,6 +161,7 @@ envlist = # Litestar {py3.8,py3.11,py3.12}-litestar-v{2.0} + {py3.8,py3.11,py3.12}-litestar-v{2.5} {py3.8,py3.11,py3.12}-litestar-latest # Loguru From e4ebb0c65c6c831afeeeb4224a785aa974448a56 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 09:53:30 -0500 Subject: [PATCH 05/29] Replace use of deprecated Dict and List with dict and list --- sentry_sdk/integrations/litestar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 32c9067b41..962e1180be 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -21,7 +21,7 @@ from litestar.data_extractors import ConnectionDataExtractor # type: ignore if TYPE_CHECKING: - from typing import Any, Dict, List, Optional, Union + from typing import Any, Optional, Union from litestar.types.asgi_types import ASGIApp # type: ignore from litestar.types import ( # type: ignore @@ -102,7 +102,7 @@ def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: def patch_middlewares() -> None: old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware - def resolve_middleware_wrapper(self: "Any") -> "List[Middleware]": + def resolve_middleware_wrapper(self: "Any") -> "list[Middleware]": return [ enable_span_for_middleware(middleware) for middleware in old__resolve_middleware_stack(self) @@ -244,7 +244,7 @@ def event_processor(event: "Event", _: "Hint") -> "Event": HTTPRoute.handle = handle_wrapper -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[Dict[str, Any]]": +def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": scope_user = scope.get("user", {}) if not scope_user: return None @@ -266,7 +266,7 @@ def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[Dict[str, Any] @ensure_integration_enabled(LitestarIntegration) def exception_handler(exc: Exception, scope: "LitestarScope") -> None: - user_info: "Optional[Dict[str, Any]]" = None + user_info: "Optional[dict[str, Any]]" = None if should_send_default_pii(): user_info = retrieve_user_from_scope(scope) if user_info and isinstance(user_info, dict): From c4a2ff710787d1eabd2fac178f1d0553b6fc189e Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 10:01:17 -0500 Subject: [PATCH 06/29] Add comment to `setup_once` describing why we ignore the "litestar" logger --- sentry_sdk/integrations/litestar.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 962e1180be..76e58df5ce 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -53,7 +53,14 @@ def setup_once() -> None: patch_middlewares() patch_http_route_handle() - # logs an error for every 500 + # The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`. + # The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following + # (among other things): + # 1. Logs them, some at least (such as 500s) as errors + # 2. Calls after_exception hooks + # The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event + # from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will + # by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so. ignore_logger("litestar") From 7ae78f1874f9c55b0c2936d93351f7dbc4cd3776 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 10:46:24 -0500 Subject: [PATCH 07/29] Replace type hints with type comments --- sentry_sdk/integrations/litestar.py | 55 ++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 76e58df5ce..3897ad8fd5 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -48,7 +48,8 @@ class LitestarIntegration(Integration): origin = f"auto.http.{identifier}" @staticmethod - def setup_once() -> None: + def setup_once(): + # type: () -> None patch_app_init() patch_middlewares() patch_http_route_handle() @@ -65,7 +66,9 @@ def setup_once() -> None: class SentryLitestarASGIMiddleware(SentryAsgiMiddleware): - def __init__(self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin): + def __init__(self, app, span_origin=LitestarIntegration.origin): + # type: (ASGIApp, str) -> None + super().__init__( app=app, unsafe_context_data=False, @@ -75,7 +78,8 @@ def __init__(self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin ) -def patch_app_init() -> None: +def patch_app_init(): + # type: () -> None """ Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the `SentryLitestarASGIMiddleware` as the outmost middleware in the stack. @@ -85,7 +89,8 @@ def patch_app_init() -> None: """ old__init__ = Litestar.__init__ - def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: + def injection_wrapper(self, *args, **kwargs): + # type: (Litestar, *Any, **Any) -> None after_exception = kwargs.pop("after_exception", []) kwargs.update( after_exception=[ @@ -106,10 +111,12 @@ def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None: Litestar.__init__ = injection_wrapper -def patch_middlewares() -> None: +def patch_middlewares(): + # type: () -> None old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware - def resolve_middleware_wrapper(self: "Any") -> "list[Middleware]": + def resolve_middleware_wrapper(self): + # type: (Any) -> list[Middleware] return [ enable_span_for_middleware(middleware) for middleware in old__resolve_middleware_stack(self) @@ -118,7 +125,8 @@ def resolve_middleware_wrapper(self: "Any") -> "list[Middleware]": BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper -def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": +def enable_span_for_middleware(middleware): + # type: (Middleware) -> Middleware if ( not hasattr(middleware, "__call__") # noqa: B004 or middleware is SentryLitestarASGIMiddleware @@ -130,12 +138,8 @@ def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": else: old_call = middleware.__call__ - async def _create_span_call( - self: "MiddlewareProtocol", - scope: "LitestarScope", - receive: "Receive", - send: "Send", - ) -> None: + async def _create_span_call(self, scope, receive, send): + # type: (MiddlewareProtocol, LitestarScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: return await old_call(self, scope, receive, send) @@ -148,9 +152,8 @@ async def _create_span_call( middleware_span.set_tag("litestar.middleware_name", middleware_name) # Creating spans for the "receive" callback - async def _sentry_receive( - *args: "Any", **kwargs: "Any" - ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": + async def _sentry_receive(*args, **kwargs): + # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage] with sentry_sdk.start_span( op=OP.MIDDLEWARE_LITESTAR_RECEIVE, description=getattr(receive, "__qualname__", str(receive)), @@ -164,7 +167,8 @@ async def _sentry_receive( new_receive = _sentry_receive if not receive_patched else receive # Creating spans for the "send" callback - async def _sentry_send(message: "Message") -> None: + async def _sentry_send(message): + # type: (Message) -> None with sentry_sdk.start_span( op=OP.MIDDLEWARE_LITESTAR_SEND, description=getattr(send, "__qualname__", str(send)), @@ -190,12 +194,12 @@ async def _sentry_send(message: "Message") -> None: return middleware -def patch_http_route_handle() -> None: +def patch_http_route_handle(): + # type: () -> None old_handle = HTTPRoute.handle - async def handle_wrapper( - self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" - ) -> None: + async def handle_wrapper(self, scope, receive, send): + # type: (HTTPRoute, HTTPScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: return await old_handle(self, scope, receive, send) @@ -210,7 +214,8 @@ async def handle_wrapper( request_data = await body - def event_processor(event: "Event", _: "Hint") -> "Event": + def event_processor(event, _): + # type: (Event, Hint) -> Event route_handler = scope.get("route_handler") request_info = event.get("request", {}) @@ -251,7 +256,8 @@ def event_processor(event: "Event", _: "Hint") -> "Event": HTTPRoute.handle = handle_wrapper -def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]": +def retrieve_user_from_scope(scope): + # type: (LitestarScope) -> Optional[dict[str, Any]] scope_user = scope.get("user", {}) if not scope_user: return None @@ -272,7 +278,8 @@ def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any] @ensure_integration_enabled(LitestarIntegration) -def exception_handler(exc: Exception, scope: "LitestarScope") -> None: +def exception_handler(exc, scope): + # type: (Exception, LitestarScope) -> None user_info: "Optional[dict[str, Any]]" = None if should_send_default_pii(): user_info = retrieve_user_from_scope(scope) From 70fc6746564016294770625454f302ebf48e24c3 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 10:54:45 -0500 Subject: [PATCH 08/29] Apply code review comments for cleaner code --- sentry_sdk/integrations/litestar.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 3897ad8fd5..e0d3c6d3e3 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -91,20 +91,13 @@ def patch_app_init(): def injection_wrapper(self, *args, **kwargs): # type: (Litestar, *Any, **Any) -> None - after_exception = kwargs.pop("after_exception", []) - kwargs.update( - after_exception=[ - exception_handler, - *( - after_exception - if isinstance(after_exception, list) - else [after_exception] - ), - ] - ) + kwargs["after_exception"] = [ + exception_handler, + *kwargs.get("after_exception", []), + ] SentryLitestarASGIMiddleware.__call__ = SentryLitestarASGIMiddleware._run_asgi3 # type: ignore - middleware = kwargs.pop("middleware", None) or [] + middleware = kwargs.get("middleware") or [] kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware] old__init__(self, *args, **kwargs) From efa5596b9a526d88c9034de22a645c707e644dd9 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 10:59:16 -0500 Subject: [PATCH 09/29] Use single underscore instead of double --- sentry_sdk/integrations/litestar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index e0d3c6d3e3..ee6d470e64 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -106,13 +106,13 @@ def injection_wrapper(self, *args, **kwargs): def patch_middlewares(): # type: () -> None - old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware + old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware def resolve_middleware_wrapper(self): # type: (Any) -> list[Middleware] return [ enable_span_for_middleware(middleware) - for middleware in old__resolve_middleware_stack(self) + for middleware in old_resolve_middleware_stack(self) ] BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper From ab5f54844f20e40320b76ad4ca904b28b9f11b71 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 11:33:46 -0500 Subject: [PATCH 10/29] Use ensure_integration_enabled where appropriate --- sentry_sdk/integrations/litestar.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index ee6d470e64..243b1f7391 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -89,6 +89,7 @@ def patch_app_init(): """ old__init__ = Litestar.__init__ + @ensure_integration_enabled(LitestarIntegration, old__init__) def injection_wrapper(self, *args, **kwargs): # type: (Litestar, *Any, **Any) -> None kwargs["after_exception"] = [ @@ -108,6 +109,7 @@ def patch_middlewares(): # type: () -> None old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware + @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) def resolve_middleware_wrapper(self): # type: (Any) -> list[Middleware] return [ @@ -131,6 +133,7 @@ def enable_span_for_middleware(middleware): else: old_call = middleware.__call__ + # @ensure_integration_enabled(LitestarIntegration, old_call) async def _create_span_call(self, scope, receive, send): # type: (MiddlewareProtocol, LitestarScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: @@ -145,6 +148,7 @@ async def _create_span_call(self, scope, receive, send): middleware_span.set_tag("litestar.middleware_name", middleware_name) # Creating spans for the "receive" callback + @ensure_integration_enabled(LitestarIntegration, receive) async def _sentry_receive(*args, **kwargs): # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage] with sentry_sdk.start_span( @@ -160,6 +164,7 @@ async def _sentry_receive(*args, **kwargs): new_receive = _sentry_receive if not receive_patched else receive # Creating spans for the "send" callback + @ensure_integration_enabled(LitestarIntegration, send) async def _sentry_send(message): # type: (Message) -> None with sentry_sdk.start_span( @@ -191,6 +196,7 @@ def patch_http_route_handle(): # type: () -> None old_handle = HTTPRoute.handle + @ensure_integration_enabled(LitestarIntegration, old_handle) async def handle_wrapper(self, scope, receive, send): # type: (HTTPRoute, HTTPScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: From 2ed697ecffe4084aa5c43149e849ca62c2044208 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 11:44:12 -0500 Subject: [PATCH 11/29] Converted type hints in local variables to type comments --- sentry_sdk/integrations/litestar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 243b1f7391..a36ea72b3c 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -129,7 +129,7 @@ def enable_span_for_middleware(middleware): return middleware if isinstance(middleware, DefineMiddleware): - old_call: "ASGIApp" = middleware.middleware.__call__ + old_call = middleware.middleware.__call__ # type: ASGIApp else: old_call = middleware.__call__ @@ -203,9 +203,9 @@ async def handle_wrapper(self, scope, receive, send): return await old_handle(self, scope, receive, send) sentry_scope = SentryScope.get_isolation_scope() - request: "Request[Any, Any]" = scope["app"].request_class( + request = scope["app"].request_class( scope=scope, receive=receive, send=send - ) + ) # type: Request[Any, Any] extracted_request_data = ConnectionDataExtractor( parse_body=True, parse_query=True )(request) @@ -279,7 +279,7 @@ def retrieve_user_from_scope(scope): @ensure_integration_enabled(LitestarIntegration) def exception_handler(exc, scope): # type: (Exception, LitestarScope) -> None - user_info: "Optional[dict[str, Any]]" = None + user_info = None # type: Optional[dict[str, Any]] if should_send_default_pii(): user_info = retrieve_user_from_scope(scope) if user_info and isinstance(user_info, dict): From a9dfc899ba4807609b988e0d75ab5ccecfb7ded0 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 13:02:48 -0500 Subject: [PATCH 12/29] Account for older minor versions of litestar that sometimes used the `Ref` type for route handler functions Rather than referring directly to the `Ref` type (which has been removed) in any way, we check for the presence of the single member `value` that `Ref` defined --- sentry_sdk/integrations/litestar.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index a36ea72b3c..cfa7eb5490 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -227,6 +227,9 @@ def event_processor(event, _): func = None if route_handler.name is not None: tx_name = route_handler.name + # Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type + elif hasattr(route_handler.fn, "value"): + func = route_handler.fn.value else: func = route_handler.fn if func is not None: From 47bca157467b3dc7fac0de1237ef98ee09432df0 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 13:05:49 -0500 Subject: [PATCH 13/29] Test for inclusion of expected route handler function name rather than equality Some older versions of litestar will end up with "partial()" rather than "some_function" --- tests/integrations/litestar/test_litestar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 99b0990d7d..dac58d86d6 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -154,7 +154,7 @@ def test_catch_exceptions( assert str(exc) == expected_message (event,) = events - assert event["transaction"] == expected_tx_name + assert expected_tx_name in event["transaction"] assert event["exception"]["values"][0]["mechanism"]["type"] == "litestar" From 47c7e0361b1a93f833e38c46e517eb021905292f Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Mon, 29 Jul 2024 14:17:21 -0500 Subject: [PATCH 14/29] Handle the fact that the earlier version supporting Python 3.12 is litestar 2.3.0 --- tox.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c3bdf5c7f2..191f1cf2f2 100644 --- a/tox.ini +++ b/tox.ini @@ -160,7 +160,10 @@ envlist = {py3.9,py3.11,py3.12}-langchain-notiktoken # Litestar - {py3.8,py3.11,py3.12}-litestar-v{2.0} + # litestar 2.0.0 is the earliest version that supports Python < 3.12 + {py3.8,py3.11}-litestar-v{2.0} + # litestar 2.3.0 is the earliest version that supports Python 3.12 + {py3.12}-litestar-v{2.3} {py3.8,py3.11,py3.12}-litestar-v{2.5} {py3.8,py3.11,py3.12}-litestar-latest @@ -500,6 +503,7 @@ deps = litestar: requests litestar: cryptography litestar-v2.0: litestar~=2.0.0 + litestar-v2.3: litestar~=2.3.0 litestar-v2.5: litestar~=2.5.0 litestar-latest: litestar From 3245b0a00bb30e3aa9aae60273e383d5747d87c1 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 10:32:02 -0500 Subject: [PATCH 15/29] Apply code review comments, particularly wrt correct usage of @ensure_integration_enabled --- sentry_sdk/integrations/litestar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index cfa7eb5490..89a7271787 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -111,7 +111,7 @@ def patch_middlewares(): @ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack) def resolve_middleware_wrapper(self): - # type: (Any) -> list[Middleware] + # type: (BaseRouteHandler) -> list[Middleware] return [ enable_span_for_middleware(middleware) for middleware in old_resolve_middleware_stack(self) @@ -133,7 +133,6 @@ def enable_span_for_middleware(middleware): else: old_call = middleware.__call__ - # @ensure_integration_enabled(LitestarIntegration, old_call) async def _create_span_call(self, scope, receive, send): # type: (MiddlewareProtocol, LitestarScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: @@ -148,9 +147,10 @@ async def _create_span_call(self, scope, receive, send): middleware_span.set_tag("litestar.middleware_name", middleware_name) # Creating spans for the "receive" callback - @ensure_integration_enabled(LitestarIntegration, receive) async def _sentry_receive(*args, **kwargs): # type: (*Any, **Any) -> Union[HTTPReceiveMessage, WebSocketReceiveMessage] + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await receive(*args, **kwargs) with sentry_sdk.start_span( op=OP.MIDDLEWARE_LITESTAR_RECEIVE, description=getattr(receive, "__qualname__", str(receive)), @@ -164,9 +164,10 @@ async def _sentry_receive(*args, **kwargs): new_receive = _sentry_receive if not receive_patched else receive # Creating spans for the "send" callback - @ensure_integration_enabled(LitestarIntegration, send) async def _sentry_send(message): # type: (Message) -> None + if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: + return await send(message) with sentry_sdk.start_span( op=OP.MIDDLEWARE_LITESTAR_SEND, description=getattr(send, "__qualname__", str(send)), @@ -196,7 +197,6 @@ def patch_http_route_handle(): # type: () -> None old_handle = HTTPRoute.handle - @ensure_integration_enabled(LitestarIntegration, old_handle) async def handle_wrapper(self, scope, receive, send): # type: (HTTPRoute, HTTPScope, Receive, Send) -> None if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: From 5ab20bc312db05a21087717a6144edddba4d454f Mon Sep 17 00:00:00 2001 From: Kelly Walker Date: Tue, 30 Jul 2024 10:47:41 -0500 Subject: [PATCH 16/29] Clean up test_middleware_spans to avoid order dependency and implementation details Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- tests/integrations/litestar/test_litestar.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index dac58d86d6..2cba391651 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -187,14 +187,16 @@ def test_middleware_spans(sentry_init, capture_events): (_, transaction_event) = events - expected = ["SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"] - - idx = 0 - for span in transaction_event["spans"]: - if span["op"] == "middleware.litestar": - assert span["description"] == expected[idx] - assert span["tags"]["litestar.middleware_name"] == expected[idx] - idx += 1 + expected = {"SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"} + found = set() + + litestar_spans = (span for span in transaction_event["spans"] if span["op"] == "middleware.litestar") + + for span in litestar_spans: + assert span["description"] in expected + assert span["description"] not in found + found.add(span["description"]) + assert span["description"] == span["tags"]["litestar.middleware_name"] def test_middleware_callback_spans(sentry_init, capture_events): From a28f32923772f30b1af3e2c2677ba6a41208a0b0 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 10:48:02 -0500 Subject: [PATCH 17/29] Use dict instead of Dict --- tests/integrations/litestar/test_litestar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index dac58d86d6..84a102e816 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -5,7 +5,7 @@ from sentry_sdk import capture_message from sentry_sdk.integrations.litestar import LitestarIntegration -from typing import Any, Dict +from typing import Any from litestar import Litestar, get, Controller from litestar.logging.config import LoggingConfig @@ -69,7 +69,7 @@ async def controller_error(self) -> None: raise Exception("Whoa") @get("/some_url") - async def homepage_handler() -> Dict[str, Any]: + async def homepage_handler() -> dict[str, Any]: 1 / 0 return {"status": "ok"} @@ -78,12 +78,12 @@ async def custom_error() -> Any: raise Exception("Too Hot") @get("/message") - async def message() -> Dict[str, Any]: + async def message() -> dict[str, Any]: capture_message("hi") return {"status": "ok"} @get("/message/{message_id:str}") - async def message_with_id() -> Dict[str, Any]: + async def message_with_id() -> dict[str, Any]: capture_message("hi") return {"status": "ok"} From d2fffbb468f6b6946f004148677cd235e92d4ce4 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 13:18:48 -0500 Subject: [PATCH 18/29] Clean up tests to avoid order dependency --- tests/integrations/litestar/test_litestar.py | 63 +++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 9a7d75124b..e1c7408783 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -189,8 +189,12 @@ def test_middleware_spans(sentry_init, capture_events): expected = {"SessionMiddleware", "LoggingMiddleware", "RateLimitMiddleware"} found = set() - - litestar_spans = (span for span in transaction_event["spans"] if span["op"] == "middleware.litestar") + + litestar_spans = ( + span + for span in transaction_event["spans"] + if span["op"] == "middleware.litestar" + ) for span in litestar_spans: assert span["description"] in expected @@ -213,9 +217,9 @@ def test_middleware_callback_spans(sentry_init, capture_events): except Exception: pass - (_, transaction_event) = events + (_, transaction_events) = events - expected = [ + expected_litestar_spans = [ { "op": "middleware.litestar", "description": "SampleMiddleware", @@ -232,10 +236,26 @@ def test_middleware_callback_spans(sentry_init, capture_events): "tags": {"litestar.middleware_name": "SampleMiddleware"}, }, ] - for idx, span in enumerate(transaction_event["spans"]): - assert span["op"] == expected[idx]["op"] - assert span["description"] == expected[idx]["description"] - assert span["tags"] == expected[idx]["tags"] + + def is_matching_span(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["op"] + and expected_span["description"] == actual_span["description"] + and expected_span["tags"] == actual_span["tags"] + ) + + actual_litestar_spans = list( + span + for span in transaction_events["spans"] + if "middleware.litestar" in span["op"] + ) + assert len(actual_litestar_spans) == 3 + + for expected_span in expected_litestar_spans: + assert any( + is_matching_span(expected_span, actual_span) + for actual_span in actual_litestar_spans + ) def test_middleware_receive_send(sentry_init, capture_events): @@ -268,9 +288,9 @@ def test_middleware_partial_receive_send(sentry_init, capture_events): except Exception: pass - (_, transaction_event) = events + (_, transaction_events) = events - expected = [ + expected_litestar_spans = [ { "op": "middleware.litestar", "description": "SamplePartialReceiveSendMiddleware", @@ -288,10 +308,25 @@ def test_middleware_partial_receive_send(sentry_init, capture_events): }, ] - for idx, span in enumerate(transaction_event["spans"]): - assert span["op"] == expected[idx]["op"] - assert span["description"].startswith(expected[idx]["description"]) - assert span["tags"] == expected[idx]["tags"] + def is_matching_span(expected_span, actual_span): + return ( + expected_span["op"] == actual_span["op"] + and actual_span["description"].startswith(expected_span["description"]) + and expected_span["tags"] == actual_span["tags"] + ) + + actual_litestar_spans = list( + span + for span in transaction_events["spans"] + if "middleware.litestar" in span["op"] + ) + assert len(actual_litestar_spans) == 3 + + for expected_span in expected_litestar_spans: + assert any( + is_matching_span(expected_span, actual_span) + for actual_span in actual_litestar_spans + ) def test_span_origin(sentry_init, capture_events): From 442ab828acc2d5d239ac3419501d223051d20be9 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 13:25:14 -0500 Subject: [PATCH 19/29] Move classes used in particular tests inside the bodies of those tests --- tests/integrations/litestar/test_litestar.py | 89 ++++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index e1c7408783..1ee0ca61e5 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -16,50 +16,6 @@ from litestar.testing import TestClient -class SampleMiddleware(AbstractMiddleware): - async def __call__(self, scope, receive, send) -> None: - async def do_stuff(message): - if message["type"] == "http.response.start": - # do something here. - pass - await send(message) - - await self.app(scope, receive, do_stuff) - - -class SampleReceiveSendMiddleware(AbstractMiddleware): - async def __call__(self, scope, receive, send): - message = await receive() - assert message - assert message["type"] == "http.request" - - send_output = await send({"type": "something-unimportant"}) - assert send_output is None - - await self.app(scope, receive, send) - - -class SamplePartialReceiveSendMiddleware(AbstractMiddleware): - async def __call__(self, scope, receive, send): - message = await receive() - assert message - assert message["type"] == "http.request" - - send_output = await send({"type": "something-unimportant"}) - assert send_output is None - - async def my_receive(*args, **kwargs): - pass - - async def my_send(*args, **kwargs): - pass - - partial_receive = functools.partial(my_receive) - partial_send = functools.partial(my_send) - - await self.app(scope, partial_receive, partial_send) - - def litestar_app_factory(middleware=None, debug=True, exception_handlers=None): class MyController(Controller): path = "/controller" @@ -204,6 +160,16 @@ def test_middleware_spans(sentry_init, capture_events): def test_middleware_callback_spans(sentry_init, capture_events): + class SampleMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send) -> None: + async def do_stuff(message): + if message["type"] == "http.response.start": + # do something here. + pass + await send(message) + + await self.app(scope, receive, do_stuff) + sentry_init( traces_sample_rate=1.0, integrations=[LitestarIntegration()], @@ -259,6 +225,17 @@ def is_matching_span(expected_span, actual_span): def test_middleware_receive_send(sentry_init, capture_events): + class SampleReceiveSendMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send): + message = await receive() + assert message + assert message["type"] == "http.request" + + send_output = await send({"type": "something-unimportant"}) + assert send_output is None + + await self.app(scope, receive, send) + sentry_init( traces_sample_rate=1.0, integrations=[LitestarIntegration()], @@ -267,14 +244,33 @@ def test_middleware_receive_send(sentry_init, capture_events): client = TestClient(litestar_app, raise_server_exceptions=False) try: - # NOTE: the assert statements checking - # for correct behaviour are in `SampleReceiveSendMiddleware`! + # See SampleReceiveSendMiddleware.__call__ above for assertions of correct behavior client.get("/message") except Exception: pass def test_middleware_partial_receive_send(sentry_init, capture_events): + class SamplePartialReceiveSendMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send): + message = await receive() + assert message + assert message["type"] == "http.request" + + send_output = await send({"type": "something-unimportant"}) + assert send_output is None + + async def my_receive(*args, **kwargs): + pass + + async def my_send(*args, **kwargs): + pass + + partial_receive = functools.partial(my_receive) + partial_send = functools.partial(my_send) + + await self.app(scope, partial_receive, partial_send) + sentry_init( traces_sample_rate=1.0, integrations=[LitestarIntegration()], @@ -284,6 +280,7 @@ def test_middleware_partial_receive_send(sentry_init, capture_events): client = TestClient(litestar_app, raise_server_exceptions=False) try: + # See SamplePartialReceiveSendMiddleware.__call__ above for assertions of correct behavior client.get("/message") except Exception: pass From 1141ce0210b765e0af91bf5a1074dac9b7b805a1 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 13:48:49 -0500 Subject: [PATCH 20/29] Remove try/catch in tests that do not expect exceptions --- tests/integrations/litestar/test_litestar.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index 1ee0ca61e5..cfa7141510 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -243,11 +243,8 @@ async def __call__(self, scope, receive, send): litestar_app = litestar_app_factory(middleware=[SampleReceiveSendMiddleware]) client = TestClient(litestar_app, raise_server_exceptions=False) - try: - # See SampleReceiveSendMiddleware.__call__ above for assertions of correct behavior - client.get("/message") - except Exception: - pass + # See SampleReceiveSendMiddleware.__call__ above for assertions of correct behavior + client.get("/message") def test_middleware_partial_receive_send(sentry_init, capture_events): @@ -279,11 +276,8 @@ async def my_send(*args, **kwargs): events = capture_events() client = TestClient(litestar_app, raise_server_exceptions=False) - try: - # See SamplePartialReceiveSendMiddleware.__call__ above for assertions of correct behavior - client.get("/message") - except Exception: - pass + # See SamplePartialReceiveSendMiddleware.__call__ above for assertions of correct behavior + client.get("/message") (_, transaction_events) = events @@ -348,10 +342,7 @@ def test_span_origin(sentry_init, capture_events): client = TestClient( litestar_app, raise_server_exceptions=False, base_url="http://testserver.local" ) - try: - client.get("/message") - except Exception: - pass + client.get("/message") (_, event) = events From db0ef8ac3c5879a313262a90b1e2dae8a758b688 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 13:56:00 -0500 Subject: [PATCH 21/29] Remove try/catch in tests that do not expect exceptions --- tests/integrations/litestar/test_litestar.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index cfa7141510..aa93fa22cd 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -136,10 +136,7 @@ def test_middleware_spans(sentry_init, capture_events): client = TestClient( litestar_app, raise_server_exceptions=False, base_url="http://testserver.local" ) - try: - client.get("/message") - except Exception: - pass + client.get("/message") (_, transaction_event) = events @@ -178,10 +175,7 @@ async def do_stuff(message): events = capture_events() client = TestClient(litestar_app, raise_server_exceptions=False) - try: - client.get("/message") - except Exception: - pass + client.get("/message") (_, transaction_events) = events From 35898699db2b666c2254fd2741fbe93ff48997d3 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 14:01:24 -0500 Subject: [PATCH 22/29] Use dict in a way that Python 3.8 and litestar can all agree on --- tests/integrations/litestar/test_litestar.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index aa93fa22cd..f025698d5d 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -1,3 +1,4 @@ +from __future__ import annotations import functools import pytest @@ -25,7 +26,7 @@ async def controller_error(self) -> None: raise Exception("Whoa") @get("/some_url") - async def homepage_handler() -> dict[str, Any]: + async def homepage_handler() -> "dict[str, Any]": 1 / 0 return {"status": "ok"} @@ -34,12 +35,12 @@ async def custom_error() -> Any: raise Exception("Too Hot") @get("/message") - async def message() -> dict[str, Any]: + async def message() -> "dict[str, Any]": capture_message("hi") return {"status": "ok"} @get("/message/{message_id:str}") - async def message_with_id() -> dict[str, Any]: + async def message_with_id() -> "dict[str, Any]": capture_message("hi") return {"status": "ok"} From b691b020003bdc4bf6c64c4d633d6637b12e52f5 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Tue, 30 Jul 2024 16:33:36 -0500 Subject: [PATCH 23/29] Added test_litestar_scope_user_on_exception_event --- tests/integrations/litestar/test_litestar.py | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/integrations/litestar/test_litestar.py b/tests/integrations/litestar/test_litestar.py index f025698d5d..90346537a7 100644 --- a/tests/integrations/litestar/test_litestar.py +++ b/tests/integrations/litestar/test_litestar.py @@ -344,3 +344,55 @@ def test_span_origin(sentry_init, capture_events): assert event["contexts"]["trace"]["origin"] == "auto.http.litestar" for span in event["spans"]: assert span["origin"] == "auto.http.litestar" + + +@pytest.mark.parametrize( + "is_send_default_pii", + [ + True, + False, + ], + ids=[ + "send_default_pii=True", + "send_default_pii=False", + ], +) +def test_litestar_scope_user_on_exception_event( + sentry_init, capture_exceptions, capture_events, is_send_default_pii +): + class TestUserMiddleware(AbstractMiddleware): + async def __call__(self, scope, receive, send): + scope["user"] = { + "email": "lennon@thebeatles.com", + "username": "john", + "id": "1", + } + await self.app(scope, receive, send) + + sentry_init( + integrations=[LitestarIntegration()], send_default_pii=is_send_default_pii + ) + litestar_app = litestar_app_factory(middleware=[TestUserMiddleware]) + exceptions = capture_exceptions() + events = capture_events() + + # This request intentionally raises an exception + client = TestClient(litestar_app) + try: + client.get("/some_url") + except Exception: + pass + + assert len(exceptions) == 1 + assert len(events) == 1 + (event,) = events + + if is_send_default_pii: + assert "user" in event + assert event["user"] == { + "email": "lennon@thebeatles.com", + "username": "john", + "id": "1", + } + else: + assert "user" not in event From 2dc2250299dda2a3bc7a88dafd777e23331c0906 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Wed, 31 Jul 2024 06:19:18 -0500 Subject: [PATCH 24/29] Remove commented out code not ported from StarliteIntegration, agreed to in code review discussion --- sentry_sdk/integrations/litestar.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 89a7271787..3bbf457c8b 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -268,14 +268,6 @@ def retrieve_user_from_scope(scope): if hasattr(scope_user, "asdict"): # dataclasses return scope_user.asdict() - # NOTE: The following section was not ported from the original StarliteIntegration based on starlite 1.x - # because as get_plugin_for_value from starlite 1.x no longer exists in litestar 2.y, and it is not clear - # what would need to be done here - # - # plugin = get_plugin_for_value(scope_user) - # if plugin and not is_async_callable(plugin.to_dict): - # return plugin.to_dict(scope_user) - return None From 4f25fe152caf8be6069b8b03afefdc35664d11c1 Mon Sep 17 00:00:00 2001 From: Kelly Walker Date: Wed, 31 Jul 2024 08:02:14 -0500 Subject: [PATCH 25/29] Update sentry_sdk/integrations/litestar.py Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- sentry_sdk/integrations/litestar.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 3bbf457c8b..d4689296a0 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -260,9 +260,7 @@ def event_processor(event, _): def retrieve_user_from_scope(scope): # type: (LitestarScope) -> Optional[dict[str, Any]] - scope_user = scope.get("user", {}) - if not scope_user: - return None + scope_user = scope.get("user") if isinstance(scope_user, dict): return scope_user if hasattr(scope_user, "asdict"): # dataclasses From cb51892280a576ff0afe6a500c1f48bb32b4f9dd Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Wed, 31 Jul 2024 10:55:29 -0500 Subject: [PATCH 26/29] Make it so the new LitestarIntegration is not auto-enabled --- sentry_sdk/integrations/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 3e1a5dea95..3c43ed5472 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -91,7 +91,6 @@ def iter_default_integrations(with_auto_enabling_integrations): "sentry_sdk.integrations.huey.HueyIntegration", "sentry_sdk.integrations.huggingface_hub.HuggingfaceHubIntegration", "sentry_sdk.integrations.langchain.LangchainIntegration", - "sentry_sdk.integrations.litestar.LitestarIntegration", "sentry_sdk.integrations.loguru.LoguruIntegration", "sentry_sdk.integrations.openai.OpenAIIntegration", "sentry_sdk.integrations.pymongo.PyMongoIntegration", From 04c4d5e20d7d3b88b67fad27d47dfae3373923d0 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Wed, 31 Jul 2024 11:06:50 -0500 Subject: [PATCH 27/29] Apply code review comments --- sentry_sdk/integrations/litestar.py | 44 +++++++++++++---------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index d4689296a0..1007432f71 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -1,11 +1,10 @@ -from typing import TYPE_CHECKING - import sentry_sdk +from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import OP from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope as SentryScope, should_send_default_pii +from sentry_sdk.scope import Scope as should_send_default_pii from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE from sentry_sdk.utils import ( ensure_integration_enabled, @@ -19,26 +18,23 @@ from litestar.middleware import DefineMiddleware # type: ignore from litestar.routes.http import HTTPRoute # type: ignore from litestar.data_extractors import ConnectionDataExtractor # type: ignore - - if TYPE_CHECKING: - from typing import Any, Optional, Union - - from litestar.types.asgi_types import ASGIApp # type: ignore - from litestar.types import ( # type: ignore - HTTPReceiveMessage, - HTTPScope, - Message, - Middleware, - Receive, - Scope as LitestarScope, - Send, - WebSocketReceiveMessage, - ) - from litestar.middleware import MiddlewareProtocol - from sentry_sdk._types import Event, Hint except ImportError: raise DidNotEnable("Litestar is not installed") - +if TYPE_CHECKING: + from typing import Any, Optional, Union + from litestar.types.asgi_types import ASGIApp # type: ignore + from litestar.types import ( # type: ignore + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Scope as LitestarScope, + Send, + WebSocketReceiveMessage, + ) + from litestar.middleware import MiddlewareProtocol + from sentry_sdk._types import Event, Hint _DEFAULT_TRANSACTION_NAME = "generic Litestar request" @@ -94,7 +90,7 @@ def injection_wrapper(self, *args, **kwargs): # type: (Litestar, *Any, **Any) -> None kwargs["after_exception"] = [ exception_handler, - *kwargs.get("after_exception", []), + *(kwargs.get("after_exception") or []), ] SentryLitestarASGIMiddleware.__call__ = SentryLitestarASGIMiddleware._run_asgi3 # type: ignore @@ -202,7 +198,7 @@ async def handle_wrapper(self, scope, receive, send): if sentry_sdk.get_client().get_integration(LitestarIntegration) is None: return await old_handle(self, scope, receive, send) - sentry_scope = SentryScope.get_isolation_scope() + sentry_scope = sentry_sdk.get_isolation_scope() request = scope["app"].request_class( scope=scope, receive=receive, send=send ) # type: Request[Any, Any] @@ -276,7 +272,7 @@ def exception_handler(exc, scope): if should_send_default_pii(): user_info = retrieve_user_from_scope(scope) if user_info and isinstance(user_info, dict): - sentry_scope = SentryScope.get_isolation_scope() + sentry_scope = sentry_sdk.get_isolation_scope() sentry_scope.set_user(user_info) event, hint = event_from_exception( From 1705c519e51161e225d802eb35830058f93f0554 Mon Sep 17 00:00:00 2001 From: "kelly.walker" Date: Wed, 31 Jul 2024 11:20:37 -0500 Subject: [PATCH 28/29] Fix typo --- sentry_sdk/integrations/litestar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/litestar.py b/sentry_sdk/integrations/litestar.py index 1007432f71..8eb3b44ca4 100644 --- a/sentry_sdk/integrations/litestar.py +++ b/sentry_sdk/integrations/litestar.py @@ -4,7 +4,7 @@ from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.scope import Scope as should_send_default_pii +from sentry_sdk.scope import should_send_default_pii from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE from sentry_sdk.utils import ( ensure_integration_enabled, From 163bd728627745779f898f7ede63b8125d22c975 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 1 Aug 2024 10:37:12 +0200 Subject: [PATCH 29/29] Trigger CI