From 9f258b08b27f39c1b0afd4f3585657c1d8e3d631 Mon Sep 17 00:00:00 2001 From: gazorby Date: Fri, 18 Nov 2022 13:43:19 +0100 Subject: [PATCH 01/24] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20starlite=20inte?= =?UTF-8?q?gration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 260 +++++++++++++++++++ tests/integrations/starlite/test_starlite.py | 0 2 files changed, 260 insertions(+) create mode 100644 sentry_sdk/integrations/starlite.py create mode 100644 tests/integrations/starlite/test_starlite.py diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py new file mode 100644 index 0000000000..5d8b8b3a21 --- /dev/null +++ b/sentry_sdk/integrations/starlite.py @@ -0,0 +1,260 @@ +from typing import TYPE_CHECKING, Any, Awaitable, Dict, List, Optional, Union + +from pydantic import BaseModel + +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.integrations import DidNotEnable, Integration +from sentry_sdk.integrations.asgi import SentryAsgiMiddleware +from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE +from sentry_sdk.utils import event_from_exception, transaction_from_function +from sentry_sdk.consts import OP + +try: + from starlite import Request, Starlite, State + from starlite.handlers.base import BaseRouteHandler + from starlite.middleware import DefineMiddleware + from starlite.plugins.base import get_plugin_for_value + from starlite.routes.http import HTTPRoute + from starlite.utils import ConnectionDataExtractor, is_async_callable + + if TYPE_CHECKING: + from starlite.types import ( + ASGIApp, + HTTPReceiveMessage, + HTTPScope, + Message, + Middleware, + Receive, + Scope, + Send, + WebSocketReceiveMessage, + ) + + from sentry_sdk._types import Event +except ImportError: + raise DidNotEnable("Starlette is not installed") + + +_DEFAULT_TRANSACTION_NAME = "generic Starlite request" + + +class SentryStarliteASGIMiddleware(SentryAsgiMiddleware): + def __init__(self, app: "ASGIApp"): + super().__init__( + app=app, + unsafe_context_data=False, + transaction_style="endpoint", + mechanism_type="asgi", + ) + + +class StarliteIntegration(Integration): + identifier = "starlite" + + @staticmethod + def setup_once(): + patch_app_init() + patch_middlewares() + patch_http_route_handle() + + +def patch_app_init() -> None: + """ + Replaces the Starlite class's `__init__` function in order to inject `after_exception` handlers and set the + `SentryStarliteASGIMiddleware` as the outmost middleware in the stack. + See: + - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception + - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ + """ + old__init__ = Starlite.__init__ # type: ignore + + def injection_wrapper(self, *args, **kwargs): + + after_exception = kwargs.pop("after_exception", []) + kwargs.update( + after_exception=[ + exception_handler, + *( + after_exception + if isinstance(after_exception, list) + else [after_exception] + ), + ] + ) + + SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3 + middleware = kwargs.pop("middleware", []) + kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] + old__init__(self, *args, **kwargs) + + Starlite.__init__ = injection_wrapper + + +def patch_middlewares() -> None: + old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware + + def resolve_middleware_wrapper(self: Any) -> List["Middleware"]: + return list( + map( + lambda middleware: enable_span_for_middleware(middleware), + old__resolve_middleware_stack(self), + ) + ) + + BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper + + +def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": + if not hasattr(middleware, "__call__"): + return middleware + + if isinstance(middleware, DefineMiddleware): + old_call = middleware.middleware.__call__ + else: + old_call = middleware.__call__ + + async def _create_span_call(self, scope: "Scope", receive: "Receive", send: "Send"): + hub = Hub.current + integration = hub.get_integration(StarliteIntegration) + if integration is not None: + middleware_name = self.__class__.__name__ + with hub.start_span( + op=OP.MIDDLEWARE_STARLITE, description=middleware_name + ) as middleware_span: + middleware_span.set_tag("starlite.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive( + *args, **kwargs + ) -> Awaitable[Union["HTTPReceiveMessage", "WebSocketReceiveMessage"]]: + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLITE_RECEIVE, + description=getattr(receive, "__qualname__", str(receive)), + ) as span: + span.set_tag("starlite.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") -> Awaitable[None]: + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLITE_SEND, + description=getattr(send, "__qualname__", str(send)), + ) as span: + span.set_tag("starlite.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) + else: + return await old_call(self, scope, receive, 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, scope: "HTTPScope", receive: "Receive", send: "Send" + ) -> None: + hub = Hub.current + integration: StarliteIntegration = hub.get_integration(StarliteIntegration) + if integration is None: + return await old_handle(self, scope, receive, send) + + with hub.configure_scope() as sentry_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", _: Dict[str, Any]) -> "Event": + route_handler = scope.get("route_handler") + + request_info = event.get("request", {}) + request_info["content_length"] = len(scope.get("_body", b"")) # type: ignore + if _should_send_default_pii(): + request_info["cookies"] = extracted_request_data["cookies"] + if request_data is not None: + request_info["data"] = request_data + + tx_name = route_handler.name or transaction_from_function( + route_handler.fn + ) + 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 = StarliteIntegration.identifier + sentry_scope.add_event_processor(event_processor) + + await old_handle(self, scope, receive, send) + + HTTPRoute.handle = handle_wrapper + + +def retrieve_user_from_scope(scope: "Scope") -> Optional[Dict[str, Any]]: + scope_user = scope.get("user", {}) + if not scope_user or not _should_send_default_pii(): + 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() + + 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 + + +def exception_handler(exc: Exception, scope: "Scope", _: "State"): + hub = Hub.current + if hub.get_integration(StarliteIntegration) is None: + return + + user_info = retrieve_user_from_scope(scope) + if user_info and isinstance(user_info, dict): + with hub.configure_scope() as sentry_scope: + sentry_scope.set_user(user_info) + + event, hint = event_from_exception( + exc, + client_options=hub.client.options if hub.client else None, + mechanism={"type": StarliteIntegration.identifier, "handled": True}, + ) + + hub.capture_event(event, hint=hint) diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py new file mode 100644 index 0000000000..e69de29bb2 From e8b707761bd5eb8811cbe456f01906008fc0c406 Mon Sep 17 00:00:00 2001 From: gazorby Date: Fri, 18 Nov 2022 17:08:29 +0100 Subject: [PATCH 02/24] =?UTF-8?q?=E2=9C=85=20test(starlite):=20add=20unit?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 7 +- setup.py | 1 + tests/integrations/starlite/test_starlite.py | 348 +++++++++++++++++++ tox.ini | 10 + 4 files changed, 364 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 5d8b8b3a21..f6e87e4f4f 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -83,7 +83,7 @@ def injection_wrapper(self, *args, **kwargs): ) SentryStarliteASGIMiddleware.__call__ = SentryStarliteASGIMiddleware._run_asgi3 - middleware = kwargs.pop("middleware", []) + middleware = kwargs.pop("middleware", None) or [] kwargs["middleware"] = [SentryStarliteASGIMiddleware, *middleware] old__init__(self, *args, **kwargs) @@ -105,7 +105,10 @@ def resolve_middleware_wrapper(self: Any) -> List["Middleware"]: def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": - if not hasattr(middleware, "__call__"): + if ( + not hasattr(middleware, "__call__") + or middleware is SentryStarliteASGIMiddleware + ): return middleware if isinstance(middleware, DefineMiddleware): diff --git a/setup.py b/setup.py index 86680690ce..67fb204e35 100644 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ def get_file_text(file_name): "chalice": ["chalice>=1.16.0"], "httpx": ["httpx>=0.16.0"], "starlette": ["starlette>=0.19.1"], + "starlite": ["starlite>=1.39.0"], "fastapi": ["fastapi>=0.79.0"], "pymongo": ["pymongo>=3.1"], "opentelemetry": ["opentelemetry-distro>=0.350b0"], diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index e69de29bb2..56a8f4a933 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -0,0 +1,348 @@ +import base64 +import functools +import os + +import pytest + +from sentry_sdk import capture_exception, capture_message, last_event_id +from sentry_sdk.integrations.starlite import StarliteIntegration +from sentry_sdk.utils import AnnotatedValue + +starlite = pytest.importorskip("starlite") + +from typing import Any, Dict + +from starlite import AbstractMiddleware, LoggingConfig, Starlite, get +from starlite.middleware import LoggingMiddlewareConfig, RateLimitConfig +from starlite.middleware.session.memory_backend import MemoryBackendConfig +from starlite.status_codes import HTTP_500_INTERNAL_SERVER_ERROR +from starlite.testing import TestClient + +PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "photo.jpg") + +BODY_JSON = {"some": "json", "for": "testing", "nested": {"numbers": 123}} + +BODY_FORM = """--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="username"\r\n\r\nJane\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="password"\r\n\r\nhello123\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="photo"; filename="photo.jpg"\r\nContent-Type: image/jpg\r\nContent-Transfer-Encoding: base64\r\n\r\n{{image_data}}\r\n--fd721ef49ea403a6--\r\n""".replace( + "{{image_data}}", str(base64.b64encode(open(PICTURE, "rb").read())) +) + +PARSED_FORM = starlite.datastructures.FormMultiDict( + [ + ("username", "Jane"), + ("password", "hello123"), + ( + "photo", + starlite.datastructures.UploadFile( + filename="photo.jpg", + file=open(PICTURE, "rb"), + content_type="image/jpeg", + ), + ), + ] +) +PARSED_BODY = { + "username": "Jane", + "password": "hello123", + "photo": AnnotatedValue( + "", {"len": 28023, "rem": [["!raw", "x", 0, 28023]]} + ), # size of photo.jpg read above +} + +# Dummy ASGI scope for creating mock Starlette requests +SCOPE = { + "client": ("172.29.0.10", 34784), + "headers": [ + [b"host", b"example.com"], + [b"user-agent", b"Mozilla/5.0 Gecko/20100101 Firefox/60.0"], + [b"content-type", b"application/json"], + [b"accept-language", b"en-US,en;q=0.5"], + [b"accept-encoding", b"gzip, deflate, br"], + [b"upgrade-insecure-requests", b"1"], + [b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"], + ], + "http_version": "0.0", + "method": "GET", + "path": "/path", + "query_string": b"qs=hello", + "scheme": "http", + "server": ("172.28.0.10", 8000), + "type": "http", +} + + +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 starlite_app_factory(middleware=None, debug=True, exception_handlers=None): + @get("/some_url") + async def homepage_handler() -> Dict[str, Any]: + 1 / 0 + return {"status": "ok"} + + @get("/custom_error") + 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 = Starlite( + route_handlers=[homepage_handler, custom_error, message, message_with_id], + debug=debug, + middleware=middleware, + logging_config=logging_config, + exception_handlers=exception_handlers, + ) + + return app + + +@pytest.mark.parametrize( + "test_url,expected_error,expected_message", + [ + ("/some_url", ZeroDivisionError, "division by zero"), + ("/custom_error", Exception, "Too Hot"), + ], +) +def test_catch_exceptions( + sentry_init, + capture_exceptions, + capture_events, + test_url, + expected_error, + expected_message, +): + sentry_init(integrations=[StarliteIntegration()]) + starlite_app = starlite_app_factory() + exceptions = capture_exceptions() + events = capture_events() + + client = TestClient(starlite_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["exception"]["values"][0]["mechanism"]["type"] == "starlite" + + +def test_middleware_spans(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[StarliteIntegration()], + ) + + logging_config = LoggingMiddlewareConfig() + session_config = MemoryBackendConfig() + rate_limit_config = RateLimitConfig(rate_limit=("hour", 5)) + + starlite_app = starlite_app_factory( + middleware=[ + session_config.middleware, + logging_config.middleware, + rate_limit_config.middleware, + ] + ) + events = capture_events() + + client = TestClient( + starlite_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.starlite": + assert span["description"] == expected[idx] + assert span["tags"]["starlite.middleware_name"] == expected[idx] + idx += 1 + + +def test_middleware_callback_spans(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[StarliteIntegration()], + ) + starlette_app = starlite_app_factory(middleware=[SampleMiddleware]) + events = capture_events() + + client = TestClient(starlette_app, raise_server_exceptions=False) + try: + client.get("/message") + except Exception: + pass + + (_, transaction_event) = events + + expected = [ + { + "op": "middleware.starlite", + "description": "SampleMiddleware", + "tags": {"starlite.middleware_name": "SampleMiddleware"}, + }, + { + "op": "middleware.starlite.send", + "description": "TestClientTransport.create_send..send", + "tags": {"starlite.middleware_name": "SampleMiddleware"}, + }, + { + "op": "middleware.starlite.send", + "description": "TestClientTransport.create_send..send", + "tags": {"starlite.middleware_name": "SampleMiddleware"}, + }, + ] + print(transaction_event["spans"]) + idx = 0 + for span in transaction_event["spans"]: + assert span["op"] == expected[idx]["op"] + assert span["description"] == expected[idx]["description"] + assert span["tags"] == expected[idx]["tags"] + idx += 1 + + +def test_middleware_receive_send(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + integrations=[StarliteIntegration()], + ) + starlette_app = starlite_app_factory(middleware=[SampleReceiveSendMiddleware]) + + client = TestClient(starlette_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=[StarliteIntegration()], + ) + starlette_app = starlite_app_factory( + middleware=[SamplePartialReceiveSendMiddleware] + ) + events = capture_events() + + client = TestClient(starlette_app, raise_server_exceptions=False) + try: + client.get("/message") + except Exception: + pass + + (_, transaction_event) = events + + expected = [ + { + "op": "middleware.starlite", + "description": "SamplePartialReceiveSendMiddleware", + "tags": {"starlite.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + { + "op": "middleware.starlite.receive", + "description": "TestClientTransport.create_receive..receive", + "tags": {"starlite.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + { + "op": "middleware.starlite.send", + "description": "TestClientTransport.create_send..send", + "tags": {"starlite.middleware_name": "SamplePartialReceiveSendMiddleware"}, + }, + ] + + print(transaction_event["spans"]) + idx = 0 + for span in transaction_event["spans"]: + assert span["op"] == expected[idx]["op"] + assert span["description"].startswith(expected[idx]["description"]) + assert span["tags"] == expected[idx]["tags"] + idx += 1 + + +def test_last_event_id(sentry_init, capture_events): + sentry_init( + integrations=[StarliteIntegration()], + ) + events = capture_events() + + def handler(request, exc): + capture_exception(exc) + return starlite.response.Response(last_event_id(), status_code=500) + + app = starlite_app_factory( + debug=False, exception_handlers={HTTP_500_INTERNAL_SERVER_ERROR: handler} + ) + + client = TestClient(app, raise_server_exceptions=False) + response = client.get("/custom_error") + assert response.status_code == 500 + print(events) + event = events[-1] + assert response.content.strip().decode("ascii") == event["event_id"] + (exception,) = event["exception"]["values"] + assert exception["type"] == "Exception" + assert exception["value"] == "Too Hot" diff --git a/tox.ini b/tox.ini index 82d66b8d6d..ccba3e0f8f 100644 --- a/tox.ini +++ b/tox.ini @@ -41,6 +41,9 @@ envlist = # Starlette {py3.7,py3.8,py3.9,py3.10}-starlette-v{0.19.1,0.20,0.21} + # Starlite + {py3.7,py3.8,py3.9,py3.10}-starlite + # Quart {py3.7,py3.8,py3.9,py3.10}-quart @@ -192,6 +195,12 @@ deps = starlette-v0.20: starlette>=0.20.0,<0.21.0 starlette-v0.21: starlette>=0.21.0,<0.22.0 + starlite: starlite + starlite: pytest-asyncio + starlite: python-multipart + starlite: requests + starlite: cryptography + fastapi: fastapi fastapi: httpx fastapi: pytest-asyncio @@ -348,6 +357,7 @@ setenv = rediscluster: TESTPATH=tests/integrations/rediscluster asgi: TESTPATH=tests/integrations/asgi starlette: TESTPATH=tests/integrations/starlette + starlite: TESTPATH=tests/integrations/starlite fastapi: TESTPATH=tests/integrations/fastapi sqlalchemy: TESTPATH=tests/integrations/sqlalchemy pure_eval: TESTPATH=tests/integrations/pure_eval From bb1ce61f174ad6430693fdcb881a98b7eb55caa1 Mon Sep 17 00:00:00 2001 From: gazorby Date: Fri, 18 Nov 2022 17:14:49 +0100 Subject: [PATCH 03/24] =?UTF-8?q?=E2=9C=85=20test:=20remove=20unneeded=20v?= =?UTF-8?q?ars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integrations/starlite/test_starlite.py | 51 -------------------- 1 file changed, 51 deletions(-) diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index 56a8f4a933..3eafad2654 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -18,57 +18,6 @@ from starlite.status_codes import HTTP_500_INTERNAL_SERVER_ERROR from starlite.testing import TestClient -PICTURE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "photo.jpg") - -BODY_JSON = {"some": "json", "for": "testing", "nested": {"numbers": 123}} - -BODY_FORM = """--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="username"\r\n\r\nJane\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="password"\r\n\r\nhello123\r\n--fd721ef49ea403a6\r\nContent-Disposition: form-data; name="photo"; filename="photo.jpg"\r\nContent-Type: image/jpg\r\nContent-Transfer-Encoding: base64\r\n\r\n{{image_data}}\r\n--fd721ef49ea403a6--\r\n""".replace( - "{{image_data}}", str(base64.b64encode(open(PICTURE, "rb").read())) -) - -PARSED_FORM = starlite.datastructures.FormMultiDict( - [ - ("username", "Jane"), - ("password", "hello123"), - ( - "photo", - starlite.datastructures.UploadFile( - filename="photo.jpg", - file=open(PICTURE, "rb"), - content_type="image/jpeg", - ), - ), - ] -) -PARSED_BODY = { - "username": "Jane", - "password": "hello123", - "photo": AnnotatedValue( - "", {"len": 28023, "rem": [["!raw", "x", 0, 28023]]} - ), # size of photo.jpg read above -} - -# Dummy ASGI scope for creating mock Starlette requests -SCOPE = { - "client": ("172.29.0.10", 34784), - "headers": [ - [b"host", b"example.com"], - [b"user-agent", b"Mozilla/5.0 Gecko/20100101 Firefox/60.0"], - [b"content-type", b"application/json"], - [b"accept-language", b"en-US,en;q=0.5"], - [b"accept-encoding", b"gzip, deflate, br"], - [b"upgrade-insecure-requests", b"1"], - [b"cookie", b"yummy_cookie=choco; tasty_cookie=strawberry"], - ], - "http_version": "0.0", - "method": "GET", - "path": "/path", - "query_string": b"qs=hello", - "scheme": "http", - "server": ("172.28.0.10", 8000), - "type": "http", -} - class SampleMiddleware(AbstractMiddleware): async def __call__(self, scope, receive, send) -> None: From 50da8156a1c25421d754ef67b275a291aa7947cb Mon Sep 17 00:00:00 2001 From: gazorby Date: Sat, 3 Dec 2022 15:58:28 +0100 Subject: [PATCH 04/24] =?UTF-8?q?=E2=9C=85=20test:=20fix=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/integrations/starlite/test_starlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index 3eafad2654..b95d732a29 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -291,7 +291,7 @@ def handler(request, exc): assert response.status_code == 500 print(events) event = events[-1] - assert response.content.strip().decode("ascii") == event["event_id"] + assert response.content.strip().decode("ascii").strip('"') == event["event_id"] (exception,) = event["exception"]["values"] assert exception["type"] == "Exception" assert exception["value"] == "Too Hot" From 4e21167a6ba935b0b8c6e2094d1d8baf0ab499e7 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sat, 3 Dec 2022 15:58:46 +0100 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=8E=A8=20style(starlite):=20cosmeti?= =?UTF-8?q?c=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index f6e87e4f4f..8b36e01878 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Any, Awaitable, Dict, List, Optional, Union +from typing import TYPE_CHECKING, Any, Awaitable, Dict, Optional, Union from pydantic import BaseModel @@ -93,13 +93,11 @@ def injection_wrapper(self, *args, **kwargs): def patch_middlewares() -> None: old__resolve_middleware_stack = BaseRouteHandler.resolve_middleware - def resolve_middleware_wrapper(self: Any) -> List["Middleware"]: - return list( - map( - lambda middleware: enable_span_for_middleware(middleware), - old__resolve_middleware_stack(self), - ) - ) + 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 @@ -143,7 +141,7 @@ 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") -> Awaitable[None]: + async def _sentry_send(message: "Message") -> None: hub = Hub.current with hub.start_span( op=OP.MIDDLEWARE_STARLITE_SEND, @@ -220,7 +218,7 @@ def event_processor(event: "Event", _: Dict[str, Any]) -> "Event": sentry_scope._name = StarliteIntegration.identifier sentry_scope.add_event_processor(event_processor) - await old_handle(self, scope, receive, send) + return await old_handle(self, scope, receive, send) HTTPRoute.handle = handle_wrapper From 730545624a799d3727353f26a793df6eeec8e4e9 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 11:54:24 +0100 Subject: [PATCH 06/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20bring=20b?= =?UTF-8?q?ack=20starlite=20consts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/consts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 00b2994ce1..2087202bad 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -63,6 +63,9 @@ class OP: MIDDLEWARE_STARLETTE = "middleware.starlette" MIDDLEWARE_STARLETTE_RECEIVE = "middleware.starlette.receive" MIDDLEWARE_STARLETTE_SEND = "middleware.starlette.send" + MIDDLEWARE_STARLITE = "middleware.starlite" + MIDDLEWARE_STARLITE_RECEIVE = "middleware.starlite.receive" + MIDDLEWARE_STARLITE_SEND = "middleware.starlite.send" QUEUE_SUBMIT_CELERY = "queue.submit.celery" QUEUE_TASK_CELERY = "queue.task.celery" QUEUE_TASK_RQ = "queue.task.rq" From 254f363ea12801d6a0732fc45bbef8405d95c6d3 Mon Sep 17 00:00:00 2001 From: gazorby Date: Sun, 8 Jan 2023 11:57:48 +0100 Subject: [PATCH 07/24] =?UTF-8?q?=F0=9F=91=B7=20build(starlite):=20add=20c?= =?UTF-8?q?i=20integration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/test-integration-starlite.yml | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/test-integration-starlite.yml diff --git a/.github/workflows/test-integration-starlite.yml b/.github/workflows/test-integration-starlite.yml new file mode 100644 index 0000000000..7b795e252d --- /dev/null +++ b/.github/workflows/test-integration-starlite.yml @@ -0,0 +1,73 @@ +name: Test starlite + +on: + push: + branches: + - master + - release/** + + pull_request: + +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +env: + BUILD_CACHE_KEY: ${{ github.sha }} + CACHED_BUILD_PATHS: | + ${{ github.workspace }}/dist-serverless + +jobs: + test: + name: starlite, python ${{ matrix.python-version }}, ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + + strategy: + fail-fast: false + matrix: + python-version: ["3.7","3.8","3.9","3.10"] + # python3.6 reached EOL and is no longer being supported on + # new versions of hosted runners on Github Actions + # ubuntu-20.04 is the last version that supported python3.6 + # see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 + os: [ubuntu-20.04] + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Setup Test Env + run: | + pip install codecov "tox>=3,<4" + + - name: Test starlite + timeout-minutes: 45 + shell: bash + run: | + set -x # print commands that are executed + coverage erase + + ./scripts/runtox.sh "${{ matrix.python-version }}-starlite" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch + coverage combine .coverage* + coverage xml -i + codecov --file coverage.xml + + check_required_tests: + name: All starlite tests passed or skipped + needs: test + # Always run this, even if a dependent job failed + if: always() + runs-on: ubuntu-20.04 + steps: + - name: Check for failures + if: contains(needs.test.result, 'failure') + run: | + echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 From 31c4c730b8aaf53e103325d8f0639ad3d23b33ea Mon Sep 17 00:00:00 2001 From: Matthieu MN <10926130+gazorby@users.noreply.github.com> Date: Sun, 8 Jan 2023 12:58:41 +0100 Subject: [PATCH 08/24] Update sentry_sdk/integrations/starlite.py Co-authored-by: Na'aman Hirschfeld --- sentry_sdk/integrations/starlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 8b36e01878..0fa0335dca 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -32,7 +32,7 @@ from sentry_sdk._types import Event except ImportError: - raise DidNotEnable("Starlette is not installed") + raise DidNotEnable("Starlite is not installed") _DEFAULT_TRANSACTION_NAME = "generic Starlite request" From 627e5cda7b58e297097c10748f1947708cb13279 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 10 Jan 2023 00:03:31 +0100 Subject: [PATCH 09/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20fix=20typ?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 0fa0335dca..5b54233cc3 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,13 +1,13 @@ -from typing import TYPE_CHECKING, Any, Awaitable, Dict, Optional, Union +from typing import TYPE_CHECKING from pydantic import BaseModel +from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE from sentry_sdk.utils import event_from_exception, transaction_from_function -from sentry_sdk.consts import OP try: from starlite import Request, Starlite, State @@ -18,6 +18,8 @@ from starlite.utils import ConnectionDataExtractor, is_async_callable if TYPE_CHECKING: + from typing import Any, Awaitable, Dict, List, Optional, Union + from starlite.types import ( ASGIApp, HTTPReceiveMessage, @@ -93,7 +95,7 @@ def injection_wrapper(self, *args, **kwargs): 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) @@ -127,7 +129,7 @@ async def _create_span_call(self, scope: "Scope", receive: "Receive", send: "Sen # Creating spans for the "receive" callback async def _sentry_receive( *args, **kwargs - ) -> Awaitable[Union["HTTPReceiveMessage", "WebSocketReceiveMessage"]]: + ) -> "Awaitable[Union[HTTPReceiveMessage, WebSocketReceiveMessage]]": hub = Hub.current with hub.start_span( op=OP.MIDDLEWARE_STARLITE_RECEIVE, @@ -191,7 +193,7 @@ async def handle_wrapper( request_data = await body - def event_processor(event: "Event", _: Dict[str, Any]) -> "Event": + def event_processor(event: "Event", _: "Dict[str, Any]") -> "Event": route_handler = scope.get("route_handler") request_info = event.get("request", {}) @@ -223,7 +225,7 @@ def event_processor(event: "Event", _: Dict[str, Any]) -> "Event": HTTPRoute.handle = handle_wrapper -def retrieve_user_from_scope(scope: "Scope") -> Optional[Dict[str, Any]]: +def retrieve_user_from_scope(scope: "Scope") -> "Optional[Dict[str, Any]]": scope_user = scope.get("user", {}) if not scope_user or not _should_send_default_pii(): return None From 911345ff7a32f4acb2601d80a0ba97883896fc03 Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 10 Jan 2023 00:19:34 +0100 Subject: [PATCH 10/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20fix=20typ?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 5b54233cc3..49f64bcff2 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -95,7 +95,7 @@ def injection_wrapper(self, *args, **kwargs): 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) From 39ab26c9332f4cd603a71fd5c1f6f0e2f5fb98ef Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 10 Jan 2023 17:01:15 +0100 Subject: [PATCH 11/24] =?UTF-8?q?=E2=9C=85=20test(starlite):=20skip=20test?= =?UTF-8?q?s=20if=20starlite=20is=20not=20installed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tool-versions | 1 + tests/integrations/starlite/__init__.py | 3 +++ tests/integrations/starlite/test_starlite.py | 3 --- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .tool-versions create mode 100644 tests/integrations/starlite/__init__.py diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000000..d316e6d5f1 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +python 3.7.12 diff --git a/tests/integrations/starlite/__init__.py b/tests/integrations/starlite/__init__.py new file mode 100644 index 0000000000..4c1037671d --- /dev/null +++ b/tests/integrations/starlite/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("starlite") diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index b95d732a29..a914b2e4cf 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -1,12 +1,9 @@ -import base64 import functools -import os import pytest from sentry_sdk import capture_exception, capture_message, last_event_id from sentry_sdk.integrations.starlite import StarliteIntegration -from sentry_sdk.utils import AnnotatedValue starlite = pytest.importorskip("starlite") From 016cfb04be8b3150f9ac232bbecda47dba3d8ddb Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 10 Jan 2023 17:41:51 +0100 Subject: [PATCH 12/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20linters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 45 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 49f64bcff2..0f42b29a5e 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -from pydantic import BaseModel +from pydantic import BaseModel # type: ignore from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii @@ -10,17 +10,16 @@ from sentry_sdk.utils import event_from_exception, transaction_from_function try: - from starlite import Request, Starlite, State - from starlite.handlers.base import BaseRouteHandler - from starlite.middleware import DefineMiddleware - from starlite.plugins.base import get_plugin_for_value - from starlite.routes.http import HTTPRoute - from starlite.utils import ConnectionDataExtractor, is_async_callable + from starlite import Request, Starlite, State # type: ignore + from starlite.handlers.base import BaseRouteHandler # type: ignore + from starlite.middleware import DefineMiddleware # type: ignore + from starlite.plugins.base import get_plugin_for_value # type: ignore + from starlite.routes.http import HTTPRoute # type: ignore + from starlite.utils import ConnectionDataExtractor, is_async_callable # type: ignore if TYPE_CHECKING: - from typing import Any, Awaitable, Dict, List, Optional, Union - - from starlite.types import ( + from typing import Any, Dict, List, Optional, Union + from starlite.types import ( # type: ignore ASGIApp, HTTPReceiveMessage, HTTPScope, @@ -31,7 +30,7 @@ Send, WebSocketReceiveMessage, ) - + from starlite import MiddlewareProtocol from sentry_sdk._types import Event except ImportError: raise DidNotEnable("Starlite is not installed") @@ -54,7 +53,7 @@ class StarliteIntegration(Integration): identifier = "starlite" @staticmethod - def setup_once(): + def setup_once() -> None: patch_app_init() patch_middlewares() patch_http_route_handle() @@ -68,9 +67,9 @@ def patch_app_init() -> None: - https://starlite-api.github.io/starlite/usage/0-the-starlite-app/5-application-hooks/#after-exception - https://starlite-api.github.io/starlite/usage/7-middleware/0-middleware-intro/ """ - old__init__ = Starlite.__init__ # type: ignore + old__init__ = Starlite.__init__ - def injection_wrapper(self, *args, **kwargs): + def injection_wrapper(self: "Starlite", *args: "Any", **kwargs: "Any") -> None: after_exception = kwargs.pop("after_exception", []) kwargs.update( @@ -106,17 +105,19 @@ def resolve_middleware_wrapper(self: "Any") -> "List[Middleware]": def enable_span_for_middleware(middleware: "Middleware") -> "Middleware": if ( - not hasattr(middleware, "__call__") + not hasattr(middleware, "__call__") # noqa: B004 or middleware is SentryStarliteASGIMiddleware ): return middleware if isinstance(middleware, DefineMiddleware): - old_call = middleware.middleware.__call__ + old_call: "ASGIApp" = middleware.middleware.__call__ else: old_call = middleware.__call__ - async def _create_span_call(self, scope: "Scope", receive: "Receive", send: "Send"): + async def _create_span_call( + self: "MiddlewareProtocol", scope: "Scope", receive: "Receive", send: "Send" + ) -> None: hub = Hub.current integration = hub.get_integration(StarliteIntegration) if integration is not None: @@ -128,8 +129,8 @@ async def _create_span_call(self, scope: "Scope", receive: "Receive", send: "Sen # Creating spans for the "receive" callback async def _sentry_receive( - *args, **kwargs - ) -> "Awaitable[Union[HTTPReceiveMessage, WebSocketReceiveMessage]]": + *args: "Any", **kwargs: "Any" + ) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]": hub = Hub.current with hub.start_span( op=OP.MIDDLEWARE_STARLITE_RECEIVE, @@ -175,7 +176,7 @@ def patch_http_route_handle() -> None: old_handle = HTTPRoute.handle async def handle_wrapper( - self, scope: "HTTPScope", receive: "Receive", send: "Send" + self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send" ) -> None: hub = Hub.current integration: StarliteIntegration = hub.get_integration(StarliteIntegration) @@ -197,7 +198,7 @@ def event_processor(event: "Event", _: "Dict[str, Any]") -> "Event": route_handler = scope.get("route_handler") request_info = event.get("request", {}) - request_info["content_length"] = len(scope.get("_body", b"")) # type: ignore + 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: @@ -244,7 +245,7 @@ def retrieve_user_from_scope(scope: "Scope") -> "Optional[Dict[str, Any]]": return None -def exception_handler(exc: Exception, scope: "Scope", _: "State"): +def exception_handler(exc: Exception, scope: "Scope", _: "State") -> None: hub = Hub.current if hub.get_integration(StarliteIntegration) is None: return From 36c61ea5ee35c27f83f0f4ebaf7c4452af11432f Mon Sep 17 00:00:00 2001 From: gazorby Date: Tue, 10 Jan 2023 18:43:41 +0100 Subject: [PATCH 13/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20transacti?= =?UTF-8?q?on=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 20 ++++++--- tests/integrations/starlite/test_starlite.py | 43 +++++++++++++++++--- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 0f42b29a5e..ca7cef8a38 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING from pydantic import BaseModel # type: ignore - +from functools import partial from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -15,7 +15,7 @@ from starlite.middleware import DefineMiddleware # type: ignore from starlite.plugins.base import get_plugin_for_value # type: ignore from starlite.routes.http import HTTPRoute # type: ignore - from starlite.utils import ConnectionDataExtractor, is_async_callable # type: ignore + from starlite.utils import ConnectionDataExtractor, is_async_callable, Ref # type: ignore if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Union @@ -204,9 +204,19 @@ def event_processor(event: "Event", _: "Dict[str, Any]") -> "Event": if request_data is not None: request_info["data"] = request_data - tx_name = route_handler.name or transaction_from_function( - route_handler.fn - ) + func = None + if route_handler.name is not None: + tx_name = route_handler.name + elif isinstance(route_handler.fn, Ref): + if isinstance(route_handler.fn.value, partial): + func = route_handler.fn.value.func + else: + func = route_handler.fn.value + 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: diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index a914b2e4cf..cd782762cb 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -9,7 +9,7 @@ from typing import Any, Dict -from starlite import AbstractMiddleware, LoggingConfig, Starlite, get +from starlite import AbstractMiddleware, LoggingConfig, Starlite, get, Controller from starlite.middleware import LoggingMiddlewareConfig, RateLimitConfig from starlite.middleware.session.memory_backend import MemoryBackendConfig from starlite.status_codes import HTTP_500_INTERNAL_SERVER_ERROR @@ -61,12 +61,19 @@ async def my_send(*args, **kwargs): def starlite_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") + @get("/custom_error", name="custom_name") async def custom_error() -> Any: raise Exception("Too Hot") @@ -83,7 +90,13 @@ async def message_with_id() -> Dict[str, Any]: logging_config = LoggingConfig() app = Starlite( - route_handlers=[homepage_handler, custom_error, message, message_with_id], + route_handlers=[ + homepage_handler, + custom_error, + message, + message_with_id, + MyController, + ], debug=debug, middleware=middleware, logging_config=logging_config, @@ -94,10 +107,26 @@ async def message_with_id() -> Dict[str, Any]: @pytest.mark.parametrize( - "test_url,expected_error,expected_message", + "test_url,expected_error,expected_message,expected_tx_name", [ - ("/some_url", ZeroDivisionError, "division by zero"), - ("/custom_error", Exception, "Too Hot"), + ( + "/some_url", + ZeroDivisionError, + "division by zero", + "tests.integrations.starlite.test_starlite.starlite_app_factory..homepage_handler", + ), + ( + "/custom_error", + Exception, + "Too Hot", + "custom_name", + ), + ( + "/controller/error", + Exception, + "Whoa", + "tests.integrations.starlite.test_starlite.starlite_app_factory..MyController.controller_error", + ), ], ) def test_catch_exceptions( @@ -107,6 +136,7 @@ def test_catch_exceptions( test_url, expected_error, expected_message, + expected_tx_name, ): sentry_init(integrations=[StarliteIntegration()]) starlite_app = starlite_app_factory() @@ -125,6 +155,7 @@ def test_catch_exceptions( (event,) = events assert event["exception"]["values"][0]["mechanism"]["type"] == "starlite" + assert event["transaction"] == expected_tx_name def test_middleware_spans(sentry_init, capture_events): From 27f92b3b49d16e9fc93888cdbba3800ce5ebf186 Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 11:53:21 +0100 Subject: [PATCH 14/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(transaction?= =?UTF-8?q?=5Ffrom=5Ffunction):=20support=20partials=20and=20partialmethod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 6 +- sentry_sdk/utils.py | 87 +++++++++++++++++------------ tests/utils/test_transaction.py | 28 ++++++++++ 3 files changed, 79 insertions(+), 42 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index ca7cef8a38..38d7c92c18 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -1,7 +1,6 @@ from typing import TYPE_CHECKING from pydantic import BaseModel # type: ignore -from functools import partial from sentry_sdk.consts import OP from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -208,10 +207,7 @@ def event_processor(event: "Event", _: "Dict[str, Any]") -> "Event": if route_handler.name is not None: tx_name = route_handler.name elif isinstance(route_handler.fn, Ref): - if isinstance(route_handler.fn.value, partial): - func = route_handler.fn.value.func - else: - func = route_handler.fn.value + func = route_handler.fn.value else: func = route_handler.fn if func is not None: diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c000a3bd2c..e229986f51 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -3,35 +3,35 @@ import linecache import logging import os +import re +import subprocess import sys import threading -import subprocess -import re import time - from datetime import datetime +from functools import partial, partialmethod import sentry_sdk -from sentry_sdk._compat import urlparse, text_type, implements_str, PY2, PY33, PY37 - +from sentry_sdk._compat import PY2, PY33, PY37, implements_str, text_type, urlparse from sentry_sdk._types import MYPY if MYPY: - from types import FrameType - from types import TracebackType - from typing import Any - from typing import Callable - from typing import Dict - from typing import ContextManager - from typing import Iterator - from typing import List - from typing import Optional - from typing import Set - from typing import Tuple - from typing import Union - from typing import Type - - from sentry_sdk._types import ExcInfo, EndpointType + from types import FrameType, TracebackType + from typing import ( + Any, + Callable, + ContextManager, + Dict, + Iterator, + List, + Optional, + Set, + Tuple, + Type, + Union, + ) + + from sentry_sdk._types import EndpointType, ExcInfo epoch = datetime(1970, 1, 1) @@ -968,9 +968,12 @@ def _get_contextvars(): """ -def transaction_from_function(func): +def qualname_from_function(func): # type: (Callable[..., Any]) -> Optional[str] - # Methods in Python 2 + """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod.""" + func_qualname = None # type: Optional[str] + + # Python 2 try: return "%s.%s.%s" % ( func.im_class.__module__, # type: ignore @@ -980,26 +983,36 @@ def transaction_from_function(func): except Exception: pass - func_qualname = ( - getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None - ) # type: Optional[str] + prefix, suffix = "", "" - if not func_qualname: - # No idea what it is - return None - - # Methods in Python 3 - # Functions - # Classes - try: - return "%s.%s" % (func.__module__, func_qualname) - except Exception: - pass + if hasattr(func, "_partialmethod") and isinstance( + func._partialmethod, partialmethod # type: ignore + ): + prefix, suffix = "partialmethod()" + func = func._partialmethod.func # type: ignore + elif isinstance(func, partial) and hasattr(func.func, "__name__"): + prefix, suffix = "partial()" + func = func.func + + if hasattr(func, "__qualname__"): + func_qualname = func.__qualname__ + elif hasattr(func, "__name__"): # Python 2.7 has no __qualname__ + func_qualname = func.__name__ + + # Python 3: methods, functions, classes + if func_qualname is not None: + if hasattr(func, "__module__"): + func_qualname = func.__module__ + "." + func_qualname + func_qualname = prefix + func_qualname + suffix - # Possibly a lambda return func_qualname +def transaction_from_function(func): + # type: (Callable[..., Any]) -> Optional[str] + return qualname_from_function(func) + + disable_capture_event = ContextVar("disable_capture_event") diff --git a/tests/utils/test_transaction.py b/tests/utils/test_transaction.py index e1aa12308f..7fb4b99cea 100644 --- a/tests/utils/test_transaction.py +++ b/tests/utils/test_transaction.py @@ -1,3 +1,5 @@ +from functools import partial, partialmethod + from sentry_sdk.utils import transaction_from_function @@ -5,11 +7,25 @@ class MyClass: def myfunc(self): pass + @partialmethod + def my_partial_method(self): + pass + def myfunc(): pass +@partial +def my_partial(): + pass + + +my_lambda = lambda: None + +my_partial_lambda = partial(lambda: None) + + def test_transaction_from_function(): x = transaction_from_function assert x(MyClass) == "tests.utils.test_transaction.MyClass" @@ -18,3 +34,15 @@ def test_transaction_from_function(): assert x(None) is None assert x(42) is None assert x(lambda: None).endswith("") + assert x(my_lambda) == "tests.utils.test_transaction." + assert ( + x(my_partial) == "partial()" + ) + assert ( + x(MyClass.my_partial_method) + == "partialmethod()" + ) + assert ( + x(my_partial_lambda) + == "partial(>)" + ) From 931ef95addcc045dd2477447c47c9064fd623208 Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 11:55:35 +0100 Subject: [PATCH 15/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20fix=20han?= =?UTF-8?q?dled=20value=20for=20uncaught=20exceptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 38d7c92c18..49377a8a9c 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -264,7 +264,7 @@ def exception_handler(exc: Exception, scope: "Scope", _: "State") -> None: event, hint = event_from_exception( exc, client_options=hub.client.options if hub.client else None, - mechanism={"type": StarliteIntegration.identifier, "handled": True}, + mechanism={"type": StarliteIntegration.identifier, "handled": False}, ) hub.capture_event(event, hint=hint) From 5b3c3db24a819db8a02860670d94b730113cbd6a Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 12:02:34 +0100 Subject: [PATCH 16/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(starlite):?= =?UTF-8?q?=20improve=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/integrations/starlite.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/starlite.py b/sentry_sdk/integrations/starlite.py index 49377a8a9c..2a5a6150bb 100644 --- a/sentry_sdk/integrations/starlite.py +++ b/sentry_sdk/integrations/starlite.py @@ -234,9 +234,8 @@ def event_processor(event: "Event", _: "Dict[str, Any]") -> "Event": def retrieve_user_from_scope(scope: "Scope") -> "Optional[Dict[str, Any]]": scope_user = scope.get("user", {}) - if not scope_user or not _should_send_default_pii(): + if not scope_user: return None - if isinstance(scope_user, dict): return scope_user if isinstance(scope_user, BaseModel): @@ -256,7 +255,9 @@ def exception_handler(exc: Exception, scope: "Scope", _: "State") -> None: if hub.get_integration(StarliteIntegration) is None: return - user_info = retrieve_user_from_scope(scope) + 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): with hub.configure_scope() as sentry_scope: sentry_scope.set_user(user_info) From d3cfd597c5165e7778af2155f734031a52e3012a Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 12:07:39 +0100 Subject: [PATCH 17/24] =?UTF-8?q?=F0=9F=90=9B=20fix(utils):=20import=20err?= =?UTF-8?q?or=20on=20python=202.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sentry_sdk/utils.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index e229986f51..4d6a091398 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -9,7 +9,14 @@ import threading import time from datetime import datetime -from functools import partial, partialmethod +from functools import partial + +try: + from functools import partialmethod + + _PARTIALMETHOD_AVAILABLE = True +except ImportError: + _PARTIALMETHOD_AVAILABLE = False import sentry_sdk from sentry_sdk._compat import PY2, PY33, PY37, implements_str, text_type, urlparse @@ -985,8 +992,10 @@ def qualname_from_function(func): prefix, suffix = "", "" - if hasattr(func, "_partialmethod") and isinstance( - func._partialmethod, partialmethod # type: ignore + if ( + _PARTIALMETHOD_AVAILABLE + and hasattr(func, "_partialmethod") + and isinstance(func._partialmethod, partialmethod) # type: ignore ): prefix, suffix = "partialmethod()" func = func._partialmethod.func # type: ignore From 5a08efdb2845bb9da0928dc068edaf3fc470368b Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 12:36:53 +0100 Subject: [PATCH 18/24] =?UTF-8?q?=F0=9F=90=9B=20fix(starlite):=20starlite?= =?UTF-8?q?=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 2 +- tests/integrations/starlite/test_starlite.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 67fb204e35..3a52ba1961 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ def get_file_text(file_name): "chalice": ["chalice>=1.16.0"], "httpx": ["httpx>=0.16.0"], "starlette": ["starlette>=0.19.1"], - "starlite": ["starlite>=1.39.0"], + "starlite": ["starlite>=1.48"], "fastapi": ["fastapi>=0.79.0"], "pymongo": ["pymongo>=3.1"], "opentelemetry": ["opentelemetry-distro>=0.350b0"], diff --git a/tests/integrations/starlite/test_starlite.py b/tests/integrations/starlite/test_starlite.py index cd782762cb..603697ce8b 100644 --- a/tests/integrations/starlite/test_starlite.py +++ b/tests/integrations/starlite/test_starlite.py @@ -64,7 +64,7 @@ def starlite_app_factory(middleware=None, debug=True, exception_handlers=None): class MyController(Controller): path = "/controller" - @get("error") + @get("/error") async def controller_error(self) -> None: raise Exception("Whoa") @@ -125,7 +125,7 @@ async def message_with_id() -> Dict[str, Any]: "/controller/error", Exception, "Whoa", - "tests.integrations.starlite.test_starlite.starlite_app_factory..MyController.controller_error", + "partial(.MyController.controller_error>)", ), ], ) From 1202902400a70cb0613bc67d5ef71de7914ec153 Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 13:04:46 +0100 Subject: [PATCH 19/24] =?UTF-8?q?=E2=9C=85=20test(transaction):=20python?= =?UTF-8?q?=202.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/utils/test_transaction.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/utils/test_transaction.py b/tests/utils/test_transaction.py index 7fb4b99cea..8e8f417d24 100644 --- a/tests/utils/test_transaction.py +++ b/tests/utils/test_transaction.py @@ -1,16 +1,20 @@ -from functools import partial, partialmethod +import sys +from functools import partial + +import pytest from sentry_sdk.utils import transaction_from_function +try: + from functools import partialmethod +except ImportError: + pass + class MyClass: def myfunc(self): pass - @partialmethod - def my_partial_method(self): - pass - def myfunc(): pass @@ -38,11 +42,22 @@ def test_transaction_from_function(): assert ( x(my_partial) == "partial()" ) - assert ( - x(MyClass.my_partial_method) - == "partialmethod()" - ) assert ( x(my_partial_lambda) == "partial(>)" ) + + +@pytest.mark.skipif(sys.version_info < (3, 4)) +def test_transaction_from_function_partialmethod(): + x = transaction_from_function + + class MyPartialClass: + @partialmethod + def my_partial_method(self): + pass + + assert ( + x(MyPartialClass.my_partial_method) + == "partialmethod()" + ) From 7292a3f0e8dcba6983c64cada9a83fb7931362d4 Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 13:12:58 +0100 Subject: [PATCH 20/24] =?UTF-8?q?=F0=9F=91=B7=20build(starlite):=20remove?= =?UTF-8?q?=20python=203.7=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-integration-starlite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-integration-starlite.yml b/.github/workflows/test-integration-starlite.yml index 7b795e252d..a8fb0e89e5 100644 --- a/.github/workflows/test-integration-starlite.yml +++ b/.github/workflows/test-integration-starlite.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7","3.8","3.9","3.10"] + python-version: ["3.8","3.9","3.10"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 From 5d7bfe9eb0ced27e502d21258a2490d53c17eb1b Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 13:33:03 +0100 Subject: [PATCH 21/24] =?UTF-8?q?=F0=9F=91=B7=20build(starlite):=20add=203?= =?UTF-8?q?.11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-integration-starlite.yml | 2 +- tox.ini | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-integration-starlite.yml b/.github/workflows/test-integration-starlite.yml index a8fb0e89e5..d63c9c541b 100644 --- a/.github/workflows/test-integration-starlite.yml +++ b/.github/workflows/test-integration-starlite.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8","3.9","3.10"] + python-version: ["3.8","3.9","3.10", "3.11"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 diff --git a/tox.ini b/tox.ini index b499936b12..a64e2d4987 100644 --- a/tox.ini +++ b/tox.ini @@ -121,9 +121,9 @@ envlist = # Starlette {py3.7,py3.8,py3.9,py3.10,py3.11}-starlette-v{0.19.1,0.20,0.21} - + # Starlite - {py3.7,py3.8,py3.9,py3.10}-starlite + {py3.8,py3.9,py3.10,py3.11}-starlite # SQL Alchemy {py2.7,py3.7,py3.8,py3.9,py3.10,py3.11}-sqlalchemy-v{1.2,1.3} @@ -342,13 +342,13 @@ deps = starlette-v0.19.1: starlette==0.19.1 starlette-v0.20: starlette>=0.20.0,<0.21.0 starlette-v0.21: starlette>=0.21.0,<0.22.0 - + # Starlite starlite: starlite starlite: pytest-asyncio starlite: python-multipart starlite: requests - starlite: cryptography + starlite: cryptography # SQLAlchemy sqlalchemy-v1.2: sqlalchemy>=1.2,<1.3 From 89d9797bfbb081eb104fd8fa8cbd8a9cbfca03fa Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 13:38:23 +0100 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=91=B7=20build(starlite):=20update?= =?UTF-8?q?=20ci=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-integration-starlite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-integration-starlite.yml b/.github/workflows/test-integration-starlite.yml index d63c9c541b..8a40f7d48c 100644 --- a/.github/workflows/test-integration-starlite.yml +++ b/.github/workflows/test-integration-starlite.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8","3.9","3.10", "3.11"] + python-version: ["3.8","3.9","3.10","3.11"] # python3.6 reached EOL and is no longer being supported on # new versions of hosted runners on Github Actions # ubuntu-20.04 is the last version that supported python3.6 From a668b59d27fd490953554638ad62741348a5ee1c Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 14:11:39 +0100 Subject: [PATCH 23/24] =?UTF-8?q?=E2=9C=85=20test(transaction):=20add=20re?= =?UTF-8?q?ason=20to=20pytest.skipif?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/utils/test_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_transaction.py b/tests/utils/test_transaction.py index 8e8f417d24..7247e61828 100644 --- a/tests/utils/test_transaction.py +++ b/tests/utils/test_transaction.py @@ -48,7 +48,7 @@ def test_transaction_from_function(): ) -@pytest.mark.skipif(sys.version_info < (3, 4)) +@pytest.mark.skipif(sys.version_info < (3, 4), reason="Require python 3.4 or higher") def test_transaction_from_function_partialmethod(): x = transaction_from_function From daa2b061ab98bf6261ed9100dfd2b540106f9c12 Mon Sep 17 00:00:00 2001 From: gazorby Date: Wed, 11 Jan 2023 14:45:43 +0100 Subject: [PATCH 24/24] =?UTF-8?q?=E2=9C=85=20test(transaction):=20fix=20as?= =?UTF-8?q?sert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/utils/test_transaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_transaction.py b/tests/utils/test_transaction.py index 7247e61828..bfb87f4c29 100644 --- a/tests/utils/test_transaction.py +++ b/tests/utils/test_transaction.py @@ -59,5 +59,5 @@ def my_partial_method(self): assert ( x(MyPartialClass.my_partial_method) - == "partialmethod()" + == "partialmethod(.MyPartialClass.my_partial_method>)" )