From 190923cc4d454bb78ef46854ae064686524d2c01 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 18 Oct 2023 13:10:08 +0200 Subject: [PATCH 01/19] Created async and sync decorators --- sentry_sdk/_types.py | 4 ++++ sentry_sdk/utils.py | 25 ++++++++++++++++++++++++- sentry_sdk/utils_py3.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 sentry_sdk/utils_py3.py diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index e88d07b420..6675a797f3 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: + from collections.abc import Awaitable from types import TracebackType from typing import Any from typing import Callable @@ -116,3 +117,6 @@ FlushedMetricValue = Union[int, float] BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal] + + GenericCallable = Callable[..., object] + GenericAsyncCallable = Callable[..., Awaitable[object]] diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index c811d2d2fe..75cbebd7ce 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -12,6 +12,7 @@ from collections import namedtuple from copy import copy from decimal import Decimal +from functools import wraps from numbers import Real try: @@ -51,6 +52,7 @@ from sentry_sdk._compat import PY2, PY33, PY37, implements_str, text_type, urlparse from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH +from sentry_sdk.hub import Hub if TYPE_CHECKING: from types import FrameType, TracebackType @@ -67,8 +69,9 @@ Type, Union, ) + from sentry_sdk.integrations import Integration - from sentry_sdk._types import EndpointType, ExcInfo + from sentry_sdk._types import EndpointType, ExcInfo, GenericCallable epoch = datetime(1970, 1, 1) @@ -1563,6 +1566,26 @@ def parse_version(version): return release_tuple +def integration_patched(original_function, integration=None): + # type: (GenericCallable, Optional[type[Integration]]) -> Callable[[GenericCallable], GenericCallable] + def patcher(sentry_patched_function): + # type: (GenericCallable) -> GenericCallable + @wraps(original_function) + def runner(*args, **kwargs): + # type: (*object, **object) -> object + if ( + integration is not None + and Hub.current.get_integration(integration) is None + ): + return original_function(*args, **kwargs) + + return sentry_patched_function(*args, **kwargs) + + return runner + + return patcher + + if PY37: def nanosecond_time(): diff --git a/sentry_sdk/utils_py3.py b/sentry_sdk/utils_py3.py new file mode 100644 index 0000000000..71a23f0980 --- /dev/null +++ b/sentry_sdk/utils_py3.py @@ -0,0 +1,31 @@ +from sentry_sdk.hub import Hub +from functools import wraps + +from sentry_sdk._types import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Optional + from sentry_sdk.integrations import Integration + + from sentry_sdk._types import GenericAsyncCallable + + +def integration_patched_async(original_function, integration=None): + # type: (GenericAsyncCallable, Optional[type[Integration]]) -> Callable[[GenericAsyncCallable], GenericAsyncCallable] + def patcher(sentry_patched_function): + # type: (GenericAsyncCallable) -> GenericAsyncCallable + @wraps(original_function) + async def runner(*args, **kwargs): + # type: (*object, **object) -> object + if ( + integration is not None + and Hub.current.get_integration(integration) is None + ): + return await original_function(*args, **kwargs) + + return await sentry_patched_function(*args, **kwargs) + + return runner + + return patcher From c8aeadbb127e7df992be56f43146c70d623712eb Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 18 Oct 2023 13:53:45 +0200 Subject: [PATCH 02/19] Added use of each sentry decorator --- sentry_sdk/_types.py | 4 +- sentry_sdk/integrations/gql.py | 8 +-- sentry_sdk/integrations/starlette.py | 98 ++++++++++++++-------------- 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 6675a797f3..66aac38125 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -118,5 +118,5 @@ BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal] - GenericCallable = Callable[..., object] - GenericAsyncCallable = Callable[..., Awaitable[object]] + GenericCallable = Callable[..., Any] + GenericAsyncCallable = Callable[..., Awaitable[Any]] diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 79fc8d022f..c05d74b3b4 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,4 +1,4 @@ -from sentry_sdk.utils import event_from_exception, parse_version +from sentry_sdk.utils import event_from_exception, integration_patched, parse_version from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -85,13 +85,11 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute + @integration_patched(original_function=real_execute, integration=GQLIntegration) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current - if hub.get_integration(GQLIntegration) is None: - return real_execute(self, document, *args, **kwargs) - - with Hub.current.configure_scope() as scope: + with hub.configure_scope() as scope: scope.add_event_processor(_make_gql_event_processor(self, document)) try: diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index ed95c757f1..bcf1706254 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -27,6 +27,7 @@ parse_version, transaction_from_function, ) +from sentry_sdk.utils_py3 import integration_patched_async if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Dict, Optional, Tuple @@ -103,61 +104,60 @@ def _enable_span_for_middleware(middleware_class): # type: (Any) -> type old_call = middleware_class.__call__ + @integration_patched_async( + original_function=old_call, integration=StarletteIntegration + ) async def _create_span_call(app, scope, receive, send, **kwargs): # type: (Any, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]], Any) -> None hub = Hub.current integration = hub.get_integration(StarletteIntegration) - if integration is not None: - middleware_name = app.__class__.__name__ - - # Update transaction name with middleware name - with hub.configure_scope() as sentry_scope: - name, source = _get_transaction_from_middleware(app, scope, integration) - if name is not None: - sentry_scope.set_transaction_name( - name, - source=source, - ) + middleware_name = app.__class__.__name__ + + # Update transaction name with middleware name + with hub.configure_scope() as sentry_scope: + name, source = _get_transaction_from_middleware(app, scope, integration) + if name is not None: + sentry_scope.set_transaction_name( + name, + source=source, + ) - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE, description=middleware_name - ) as middleware_span: - middleware_span.set_tag("starlette.middleware_name", middleware_name) - - # Creating spans for the "receive" callback - async def _sentry_receive(*args, **kwargs): - # type: (*Any, **Any) -> Any - hub = Hub.current - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - description=getattr(receive, "__qualname__", str(receive)), - ) as span: - span.set_tag("starlette.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(*args, **kwargs): - # type: (*Any, **Any) -> Any - hub = Hub.current - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - description=getattr(send, "__qualname__", str(send)), - ) as span: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - 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(app, scope, new_receive, new_send, **kwargs) + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE, description=middleware_name + ) as middleware_span: + middleware_span.set_tag("starlette.middleware_name", middleware_name) - else: - return await old_call(app, scope, receive, send, **kwargs) + # Creating spans for the "receive" callback + async def _sentry_receive(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + description=getattr(receive, "__qualname__", str(receive)), + ) as span: + span.set_tag("starlette.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(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + description=getattr(send, "__qualname__", str(send)), + ) as span: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + 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(app, scope, new_receive, new_send, **kwargs) not_yet_patched = old_call.__name__ not in [ "_create_span_call", From c18a9025d31cedec30b55d0a9fd55561fc98b16b Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 18 Oct 2023 15:09:19 +0200 Subject: [PATCH 03/19] Fix circular import --- sentry_sdk/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 75cbebd7ce..051632e241 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -49,10 +49,10 @@ _PARTIALMETHOD_AVAILABLE = False import sentry_sdk +import sentry_sdk.hub from sentry_sdk._compat import PY2, PY33, PY37, implements_str, text_type, urlparse from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH -from sentry_sdk.hub import Hub if TYPE_CHECKING: from types import FrameType, TracebackType @@ -1575,7 +1575,7 @@ def runner(*args, **kwargs): # type: (*object, **object) -> object if ( integration is not None - and Hub.current.get_integration(integration) is None + and sentry_sdk.hub.Hub.current.get_integration(integration) is None ): return original_function(*args, **kwargs) From 9f5c2797f515340e8ecdd9f0e3f59def22c73ecd Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 11:57:54 +0100 Subject: [PATCH 04/19] Revert changes to starlette.py --- sentry_sdk/integrations/starlette.py | 96 ++++++++++++++-------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index 5715d99ed1..ecbc0cafe7 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -25,7 +25,6 @@ parse_version, transaction_from_function, ) -from sentry_sdk.utils_py3 import integration_patched_async if TYPE_CHECKING: from typing import Any, Awaitable, Callable, Dict, Optional, Tuple @@ -103,59 +102,60 @@ def _enable_span_for_middleware(middleware_class): # type: (Any) -> type old_call = middleware_class.__call__ - @integration_patched_async( - original_function=old_call, integration=StarletteIntegration - ) async def _create_span_call(app, scope, receive, send, **kwargs): # type: (Any, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]], Any) -> None hub = Hub.current integration = hub.get_integration(StarletteIntegration) - middleware_name = app.__class__.__name__ - - # Update transaction name with middleware name - name, source = _get_transaction_from_middleware(app, scope, integration) - if name is not None: - Scope.get_current_scope().set_transaction_name( - name, - source=source, - ) + if integration is not None: + middleware_name = app.__class__.__name__ + + # Update transaction name with middleware name + name, source = _get_transaction_from_middleware(app, scope, integration) + if name is not None: + Scope.get_current_scope().set_transaction_name( + name, + source=source, + ) - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE, description=middleware_name - ) as middleware_span: - middleware_span.set_tag("starlette.middleware_name", middleware_name) + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE, description=middleware_name + ) as middleware_span: + middleware_span.set_tag("starlette.middleware_name", middleware_name) + + # Creating spans for the "receive" callback + async def _sentry_receive(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE_RECEIVE, + description=getattr(receive, "__qualname__", str(receive)), + ) as span: + span.set_tag("starlette.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(*args, **kwargs): + # type: (*Any, **Any) -> Any + hub = Hub.current + with hub.start_span( + op=OP.MIDDLEWARE_STARLETTE_SEND, + description=getattr(send, "__qualname__", str(send)), + ) as span: + span.set_tag("starlette.middleware_name", middleware_name) + return await send(*args, **kwargs) + + 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(app, scope, new_receive, new_send, **kwargs) - # Creating spans for the "receive" callback - async def _sentry_receive(*args, **kwargs): - # type: (*Any, **Any) -> Any - hub = Hub.current - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE_RECEIVE, - description=getattr(receive, "__qualname__", str(receive)), - ) as span: - span.set_tag("starlette.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(*args, **kwargs): - # type: (*Any, **Any) -> Any - hub = Hub.current - with hub.start_span( - op=OP.MIDDLEWARE_STARLETTE_SEND, - description=getattr(send, "__qualname__", str(send)), - ) as span: - span.set_tag("starlette.middleware_name", middleware_name) - return await send(*args, **kwargs) - - 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(app, scope, new_receive, new_send, **kwargs) + else: + return await old_call(app, scope, receive, send, **kwargs) not_yet_patched = old_call.__name__ not in [ "_create_span_call", From 45b90ab5c619cf70de4550e9eb62832d6ccfad19 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 11:59:23 +0100 Subject: [PATCH 05/19] Rename method --- sentry_sdk/integrations/gql.py | 10 ++++++++-- sentry_sdk/utils.py | 2 +- sentry_sdk/utils_py3.py | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index c292937498..f282fad8a3 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,4 +1,8 @@ -from sentry_sdk.utils import event_from_exception, integration_patched, parse_version +from sentry_sdk.utils import ( + event_from_exception, + ensure_integration_enabled, + parse_version, +) from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -85,7 +89,9 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute - @integration_patched(original_function=real_execute, integration=GQLIntegration) + @ensure_integration_enabled( + original_function=real_execute, integration=GQLIntegration + ) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 790b7329ff..85fb5ee915 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1624,7 +1624,7 @@ def reraise(tp, value, tb=None): raise value -def integration_patched(original_function, integration=None): +def ensure_integration_enabled(original_function, integration=None): # type: (GenericCallable, Optional[type[Integration]]) -> Callable[[GenericCallable], GenericCallable] def patcher(sentry_patched_function): # type: (GenericCallable) -> GenericCallable diff --git a/sentry_sdk/utils_py3.py b/sentry_sdk/utils_py3.py index 71a23f0980..0cd810253f 100644 --- a/sentry_sdk/utils_py3.py +++ b/sentry_sdk/utils_py3.py @@ -11,7 +11,7 @@ from sentry_sdk._types import GenericAsyncCallable -def integration_patched_async(original_function, integration=None): +def ensure_integration_enabled_async(original_function, integration=None): # type: (GenericAsyncCallable, Optional[type[Integration]]) -> Callable[[GenericAsyncCallable], GenericAsyncCallable] def patcher(sentry_patched_function): # type: (GenericAsyncCallable) -> GenericAsyncCallable From 69ebafb0d7124b7372e161a22af91df028734bc5 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 12:07:51 +0100 Subject: [PATCH 06/19] Use actual generics, move async implementation to utils --- sentry_sdk/_types.py | 5 +---- sentry_sdk/utils.py | 38 ++++++++++++++++++++++++++++++++++---- sentry_sdk/utils_py3.py | 31 ------------------------------- 3 files changed, 35 insertions(+), 39 deletions(-) delete mode 100644 sentry_sdk/utils_py3.py diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 7fdf5e3e8d..bfd1a97d90 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -9,7 +9,7 @@ if TYPE_CHECKING: - from collections.abc import Awaitable, MutableMapping + from collections.abc import MutableMapping from datetime import datetime @@ -177,7 +177,4 @@ BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal] - GenericCallable = Callable[..., Any] - GenericAsyncCallable = Callable[..., Awaitable[Any]] - MetricMetaKey = Tuple[MetricType, str, MeasurementUnit] diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 85fb5ee915..adf31fb6a4 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -32,6 +32,8 @@ from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType if TYPE_CHECKING: + from collections.abc import Awaitable + from types import FrameType, TracebackType from typing import ( Any, @@ -42,14 +44,19 @@ List, NoReturn, Optional, + ParamSpec, Set, Tuple, Type, + TypeVar, Union, ) from sentry_sdk.integrations import Integration - from sentry_sdk._types import Event, ExcInfo, GenericCallable + from sentry_sdk._types import Event, ExcInfo + + P = ParamSpec("P") + R = TypeVar("R") epoch = datetime(1970, 1, 1) @@ -1625,12 +1632,12 @@ def reraise(tp, value, tb=None): def ensure_integration_enabled(original_function, integration=None): - # type: (GenericCallable, Optional[type[Integration]]) -> Callable[[GenericCallable], GenericCallable] + # type: (Callable[P, R], Optional[type[Integration]]) -> Callable[[Callable[P, R]], Callable[P, R]] def patcher(sentry_patched_function): - # type: (GenericCallable) -> GenericCallable + # type: (Callable[P, R]) -> Callable[P, R] @wraps(original_function) def runner(*args, **kwargs): - # type: (*object, **object) -> object + # type: (*object, **object) -> R if ( integration is not None and sentry_sdk.hub.Hub.current.get_integration(integration) is None @@ -1644,6 +1651,29 @@ def runner(*args, **kwargs): return patcher +def ensure_integration_enabled_async( + original_function, # type: Callable[P, Awaitable[R]] + integration=None, # type: Optional[type[Integration]] +): + # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] + def patcher(sentry_patched_function): + # type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] + @wraps(original_function) + async def runner(*args, **kwargs): + # type: (*object, **object) -> R + if ( + integration is not None + and sentry_sdk.hub.Hub.current.get_integration(integration) is None + ): + return await original_function(*args, **kwargs) + + return await sentry_patched_function(*args, **kwargs) + + return runner + + return patcher + + if PY37: def nanosecond_time(): diff --git a/sentry_sdk/utils_py3.py b/sentry_sdk/utils_py3.py deleted file mode 100644 index 0cd810253f..0000000000 --- a/sentry_sdk/utils_py3.py +++ /dev/null @@ -1,31 +0,0 @@ -from sentry_sdk.hub import Hub -from functools import wraps - -from sentry_sdk._types import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Callable - from typing import Optional - from sentry_sdk.integrations import Integration - - from sentry_sdk._types import GenericAsyncCallable - - -def ensure_integration_enabled_async(original_function, integration=None): - # type: (GenericAsyncCallable, Optional[type[Integration]]) -> Callable[[GenericAsyncCallable], GenericAsyncCallable] - def patcher(sentry_patched_function): - # type: (GenericAsyncCallable) -> GenericAsyncCallable - @wraps(original_function) - async def runner(*args, **kwargs): - # type: (*object, **object) -> object - if ( - integration is not None - and Hub.current.get_integration(integration) is None - ): - return await original_function(*args, **kwargs) - - return await sentry_patched_function(*args, **kwargs) - - return runner - - return patcher From 46cd0e2715a548452b45ef3f262378c44b0551ac Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 12:16:18 +0100 Subject: [PATCH 07/19] Refactor parameters --- sentry_sdk/integrations/gql.py | 4 +--- sentry_sdk/utils.py | 19 ++++++++----------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index f282fad8a3..23e42a5651 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -89,9 +89,7 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute - @ensure_integration_enabled( - original_function=real_execute, integration=GQLIntegration - ) + @ensure_integration_enabled(GQLIntegration, real_execute) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index adf31fb6a4..b014dd605a 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1631,17 +1631,17 @@ def reraise(tp, value, tb=None): raise value -def ensure_integration_enabled(original_function, integration=None): - # type: (Callable[P, R], Optional[type[Integration]]) -> Callable[[Callable[P, R]], Callable[P, R]] +def ensure_integration_enabled( + integration, # type: type[Integration] + original_function, # type: Callable[P, R] +): + # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] def patcher(sentry_patched_function): # type: (Callable[P, R]) -> Callable[P, R] @wraps(original_function) def runner(*args, **kwargs): # type: (*object, **object) -> R - if ( - integration is not None - and sentry_sdk.hub.Hub.current.get_integration(integration) is None - ): + if sentry_sdk.hub.Hub.current.get_integration(integration) is None: return original_function(*args, **kwargs) return sentry_patched_function(*args, **kwargs) @@ -1652,8 +1652,8 @@ def runner(*args, **kwargs): def ensure_integration_enabled_async( + integration, # type: type[Integration] original_function, # type: Callable[P, Awaitable[R]] - integration=None, # type: Optional[type[Integration]] ): # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] def patcher(sentry_patched_function): @@ -1661,10 +1661,7 @@ def patcher(sentry_patched_function): @wraps(original_function) async def runner(*args, **kwargs): # type: (*object, **object) -> R - if ( - integration is not None - and sentry_sdk.hub.Hub.current.get_integration(integration) is None - ): + if sentry_sdk.hub.Hub.current.get_integration(integration) is None: return await original_function(*args, **kwargs) return await sentry_patched_function(*args, **kwargs) From 7a8196a05cc079a8a04aa832d6b3265f1e3a5ab5 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 12:16:54 +0100 Subject: [PATCH 08/19] Undo changes to _types.py --- sentry_sdk/_types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index bfd1a97d90..10c26a1e6b 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -176,5 +176,4 @@ FlushedMetricValue = Union[int, float] BucketKey = Tuple[MetricType, str, MeasurementUnit, MetricTagsInternal] - MetricMetaKey = Tuple[MetricType, str, MeasurementUnit] From d9016dbbeacde301dfe122f882ef2a2e0872d31f Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 12:19:34 +0100 Subject: [PATCH 09/19] Use client instead of Hub --- sentry_sdk/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index b014dd605a..f67a6d1ff2 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1641,7 +1641,7 @@ def patcher(sentry_patched_function): @wraps(original_function) def runner(*args, **kwargs): # type: (*object, **object) -> R - if sentry_sdk.hub.Hub.current.get_integration(integration) is None: + if sentry_sdk.get_client().get_integration(integration) is None: return original_function(*args, **kwargs) return sentry_patched_function(*args, **kwargs) @@ -1661,7 +1661,7 @@ def patcher(sentry_patched_function): @wraps(original_function) async def runner(*args, **kwargs): # type: (*object, **object) -> R - if sentry_sdk.hub.Hub.current.get_integration(integration) is None: + if sentry_sdk.get_client().get_integration(integration) is None: return await original_function(*args, **kwargs) return await sentry_patched_function(*args, **kwargs) From 75934d1e26c00b104af92a5678add964a96daf08 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 13:27:19 +0100 Subject: [PATCH 10/19] Add doc string --- sentry_sdk/utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index f67a6d1ff2..0e98c81411 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1635,6 +1635,28 @@ def ensure_integration_enabled( integration, # type: type[Integration] original_function, # type: Callable[P, R] ): + """ + Ensures a given integration is enabled prior to calling a Sentry-patched function. + + The function takes as its parameters the integration that must be enabled and the original + function that the SDK is patching. The function returns a function that takes the + decorated (Sentry-patched) function as its parameter, and returns a function that, when + called, checks whether the given integration is enabled. If the integration is enabled, the + funciton calls the decorated, Sentry-patched funciton. If the integration is not enabled, + the original function is called. + + The function also takes care of preserving the original function's signature and docstring. + + Example usage: + + ```python + @ensure_integration_enabled(MyIntegration, my_function) + def patch_my_function(): + with sentry_sdk.start_transaction(...): + return my_function() + ``` + """ + # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] def patcher(sentry_patched_function): # type: (Callable[P, R]) -> Callable[P, R] @@ -1655,6 +1677,12 @@ def ensure_integration_enabled_async( integration, # type: type[Integration] original_function, # type: Callable[P, Awaitable[R]] ): + """ + Version of `ensure_integration_enabled` for decorating async functions. + + Please refer to the `ensure_integration_enabled` documentation for more information. + """ + # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] def patcher(sentry_patched_function): # type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] From 66726d05fdab6672aed4b1a8ef053261d7939821 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Fri, 15 Mar 2024 13:48:12 +0100 Subject: [PATCH 11/19] Move type comments --- sentry_sdk/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 0e98c81411..e00fcc6f93 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1635,6 +1635,7 @@ def ensure_integration_enabled( integration, # type: type[Integration] original_function, # type: Callable[P, R] ): + # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] """ Ensures a given integration is enabled prior to calling a Sentry-patched function. @@ -1657,7 +1658,6 @@ def patch_my_function(): ``` """ - # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] def patcher(sentry_patched_function): # type: (Callable[P, R]) -> Callable[P, R] @wraps(original_function) @@ -1677,13 +1677,13 @@ def ensure_integration_enabled_async( integration, # type: type[Integration] original_function, # type: Callable[P, Awaitable[R]] ): + # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] """ Version of `ensure_integration_enabled` for decorating async functions. Please refer to the `ensure_integration_enabled` documentation for more information. """ - # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] def patcher(sentry_patched_function): # type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] @wraps(original_function) From e8c921c3f34f26f31628b960110069a729d009bb Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 18 Mar 2024 11:02:30 +0100 Subject: [PATCH 12/19] Fix mypy --- sentry_sdk/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index e00fcc6f93..fcdaf4182a 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -1661,8 +1661,8 @@ def patch_my_function(): def patcher(sentry_patched_function): # type: (Callable[P, R]) -> Callable[P, R] @wraps(original_function) - def runner(*args, **kwargs): - # type: (*object, **object) -> R + def runner(*args: "P.args", **kwargs: "P.kwargs"): + # type: (...) -> R if sentry_sdk.get_client().get_integration(integration) is None: return original_function(*args, **kwargs) @@ -1687,8 +1687,8 @@ def ensure_integration_enabled_async( def patcher(sentry_patched_function): # type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] @wraps(original_function) - async def runner(*args, **kwargs): - # type: (*object, **object) -> R + async def runner(*args: "P.args", **kwargs: "P.kwargs"): + # type: (...) -> R if sentry_sdk.get_client().get_integration(integration) is None: return await original_function(*args, **kwargs) From 20688fd1adacae972cc27ef01de29cf944e74909 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 18 Mar 2024 11:11:30 +0100 Subject: [PATCH 13/19] Fix circular import --- sentry_sdk/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index fcdaf4182a..fa4eeca07e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -51,8 +51,8 @@ TypeVar, Union, ) - from sentry_sdk.integrations import Integration + import sentry_sdk.integrations from sentry_sdk._types import Event, ExcInfo P = ParamSpec("P") @@ -1632,7 +1632,7 @@ def reraise(tp, value, tb=None): def ensure_integration_enabled( - integration, # type: type[Integration] + integration, # type: type[sentry_sdk.integrations.Integration] original_function, # type: Callable[P, R] ): # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]] @@ -1674,7 +1674,7 @@ def runner(*args: "P.args", **kwargs: "P.kwargs"): def ensure_integration_enabled_async( - integration, # type: type[Integration] + integration, # type: type[sentry_sdk.integrations.Integration] original_function, # type: Callable[P, Awaitable[R]] ): # type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]] From d93ff9294fcfc5fb83f526b9e1d63e931b7945a2 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 18 Mar 2024 12:44:04 +0100 Subject: [PATCH 14/19] Added unit tests for decorators --- tests/test_utils.py | 85 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 22a5a89978..e5dda7d57e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ import pytest import sentry_sdk +from sentry_sdk.integrations import Integration from sentry_sdk.utils import ( Components, Dsn, @@ -21,9 +22,21 @@ serialize_frame, is_sentry_url, _get_installed_modules, + ensure_integration_enabled, + ensure_integration_enabled_async, ) +class TestIntegration(Integration): + """ + Test integration for testing ensure_integration_enabled and + ensure_integration_enabled_async decorators. + """ + + identifier = "test" + setup_once = mock.MagicMock() + + def _normalize_distribution_name(name): # type: (str) -> str """Normalize distribution name according to PEP-0503. @@ -567,3 +580,75 @@ def test_default_release_empty_string(): release = get_default_release() assert release is None + + +def test_ensure_integration_enabled_integration_enabled(sentry_init): + def original_function(): + return "original" + + def function_to_patch(): + return "patched" + + sentry_init(integrations=[TestIntegration()]) + + # Test the decorator by applying to function_to_patch + patched_function = ensure_integration_enabled(TestIntegration, original_function)( + function_to_patch + ) + + assert patched_function() == "patched" + + +def test_ensure_integration_enabled_integration_disabled(sentry_init): + def original_function(): + return "original" + + def function_to_patch(): + return "patched" + + sentry_init(integrations=[]) # TestIntegration is disabled + + # Test the decorator by applying to function_to_patch + patched_function = ensure_integration_enabled(TestIntegration, original_function)( + function_to_patch + ) + + assert patched_function() == "original" + + +@pytest.mark.asyncio +async def test_ensure_integration_enabled_async_integration_enabled(sentry_init): + # Setup variables and functions for the test + async def original_function(): + return "original" + + async def function_to_patch(): + return "patched" + + sentry_init(integrations=[TestIntegration()]) + + # Test the decorator by applying to function_to_patch + patched_function = ensure_integration_enabled_async( + TestIntegration, original_function + )(function_to_patch) + + assert await patched_function() == "patched" + + +@pytest.mark.asyncio +async def test_ensure_integration_enabled_async_integration_disabled(sentry_init): + # Setup variables and functions for the test + async def original_function(): + return "original" + + async def function_to_patch(): + return "patched" + + sentry_init(integrations=[]) # TestIntegration is disabled + + # Test the decorator by applying to function_to_patch + patched_function = ensure_integration_enabled_async( + TestIntegration, original_function + )(function_to_patch) + + assert await patched_function() == "original" From 85c1a1f3f1a94564d070bfd97e96e798b7a30ef0 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 18 Mar 2024 12:56:47 +0100 Subject: [PATCH 15/19] Revert gql changes --- sentry_sdk/integrations/gql.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 23e42a5651..9db6632a4a 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,8 +1,4 @@ -from sentry_sdk.utils import ( - event_from_exception, - ensure_integration_enabled, - parse_version, -) +from sentry_sdk.utils import event_from_exception, parse_version from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -89,11 +85,13 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute - @ensure_integration_enabled(GQLIntegration, real_execute) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current - with hub.configure_scope() as scope: + if hub.get_integration(GQLIntegration) is None: + return real_execute(self, document, *args, **kwargs) + + with Hub.current.configure_scope() as scope: scope.add_event_processor(_make_gql_event_processor(self, document)) try: From 682eb71829baaddced06c121440be70ce83c644e Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Mon, 18 Mar 2024 12:59:16 +0100 Subject: [PATCH 16/19] Revert "Revert gql changes" This reverts commit 85c1a1f3f1a94564d070bfd97e96e798b7a30ef0. --- sentry_sdk/integrations/gql.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 9db6632a4a..23e42a5651 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,4 +1,8 @@ -from sentry_sdk.utils import event_from_exception, parse_version +from sentry_sdk.utils import ( + event_from_exception, + ensure_integration_enabled, + parse_version, +) from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -85,13 +89,11 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute + @ensure_integration_enabled(GQLIntegration, real_execute) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current - if hub.get_integration(GQLIntegration) is None: - return real_execute(self, document, *args, **kwargs) - - with Hub.current.configure_scope() as scope: + with hub.configure_scope() as scope: scope.add_event_processor(_make_gql_event_processor(self, document)) try: From c4c4605b6c70ad2e09cf84763315e0a2aec61b03 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 19 Mar 2024 11:22:58 +0100 Subject: [PATCH 17/19] ref: Shortcut for `should_send_default_pii` --- sentry_sdk/hub.py | 2 +- sentry_sdk/scope.py | 6 ++++++ tests/test_scope.py | 20 +++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 2af3091f5d..8ac2348597 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -60,7 +60,7 @@ def overload(x): def _should_send_default_pii(): # type: () -> bool - # TODO: Migrate existing code to client.should_send_default_pii() and remove this function. + # TODO: Migrate existing code to `scope.should_send_default_pii()` and remove this function. # New code should not use this function! client = Hub.current.client if not client: diff --git a/sentry_sdk/scope.py b/sentry_sdk/scope.py index 5b92bf7433..b173e13303 100644 --- a/sentry_sdk/scope.py +++ b/sentry_sdk/scope.py @@ -1658,5 +1658,11 @@ def use_isolation_scope(isolation_scope): _isolation_scope.reset(isolation_token) +def should_send_default_pii(): + # type: () -> bool + """Shortcut for `Scope.get_client().should_send_default_pii()`.""" + return Scope.get_client().should_send_default_pii() + + # Circular imports from sentry_sdk.client import NonRecordingClient diff --git a/tests/test_scope.py b/tests/test_scope.py index a1d7d8c397..d5910a8c1d 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -10,7 +10,13 @@ new_scope, ) from sentry_sdk.client import Client, NonRecordingClient -from sentry_sdk.scope import Scope, ScopeType, use_isolation_scope, use_scope +from sentry_sdk.scope import ( + Scope, + ScopeType, + use_isolation_scope, + use_scope, + should_send_default_pii, +) def test_copying(): @@ -778,3 +784,15 @@ def test_nested_scopes_with_tags(sentry_init, capture_envelopes): assert transaction["tags"] == {"isolation_scope1": 1, "current_scope2": 1, "trx": 1} assert transaction["spans"][0]["tags"] == {"a": 1} assert transaction["spans"][1]["tags"] == {"b": 1} + + +def test_should_send_default_pii_true(sentry_init): + sentry_init(send_default_pii=True) + + assert should_send_default_pii() is True + + +def test_should_send_default_pii_false(sentry_init): + sentry_init(send_default_pii=False) + + assert should_send_default_pii() is False From 8205626ce101df5bb4c97f45a4245f0f4b783e0a Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 19 Mar 2024 11:30:34 +0100 Subject: [PATCH 18/19] revert gql changes --- sentry_sdk/integrations/gql.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/sentry_sdk/integrations/gql.py b/sentry_sdk/integrations/gql.py index 23e42a5651..9db6632a4a 100644 --- a/sentry_sdk/integrations/gql.py +++ b/sentry_sdk/integrations/gql.py @@ -1,8 +1,4 @@ -from sentry_sdk.utils import ( - event_from_exception, - ensure_integration_enabled, - parse_version, -) +from sentry_sdk.utils import event_from_exception, parse_version from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.integrations import DidNotEnable, Integration @@ -89,11 +85,13 @@ def _patch_execute(): # type: () -> None real_execute = gql.Client.execute - @ensure_integration_enabled(GQLIntegration, real_execute) def sentry_patched_execute(self, document, *args, **kwargs): # type: (gql.Client, DocumentNode, Any, Any) -> Any hub = Hub.current - with hub.configure_scope() as scope: + if hub.get_integration(GQLIntegration) is None: + return real_execute(self, document, *args, **kwargs) + + with Hub.current.configure_scope() as scope: scope.add_event_processor(_make_gql_event_processor(self, document)) try: From e37fbe0d972c42244138b938469c665f46f9d966 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Tue, 19 Mar 2024 11:35:09 +0100 Subject: [PATCH 19/19] ref: Use `scope.should_send_default_pii` in FastAPI integration --- sentry_sdk/integrations/fastapi.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py index 27624ed817..a6fed8f620 100644 --- a/sentry_sdk/integrations/fastapi.py +++ b/sentry_sdk/integrations/fastapi.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk._types import TYPE_CHECKING from sentry_sdk.integrations import DidNotEnable -from sentry_sdk.scope import Scope +from sentry_sdk.scope import Scope, should_send_default_pii from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE from sentry_sdk.utils import ( transaction_from_function, @@ -118,10 +118,7 @@ def event_processor(event, hint): # Extract information from request request_info = event.get("request", {}) if info: - if ( - "cookies" in info - and sentry_sdk.get_client().should_send_default_pii() - ): + if "cookies" in info and should_send_default_pii(): request_info["cookies"] = info["cookies"] if "data" in info: request_info["data"] = info["data"]