Skip to content

Commit 7a2c153

Browse files
ref: Patched functions decorator for integrations (getsentry#2454)
This commit introduces two new decorators in sentry_sdk.utils that we can use in our integrations to automate the checks for whether the integration is still enabled. Since these decorators use the new scopes API, adopting these decorators may simplify the change to the new Scopes API.
1 parent 0a47317 commit 7a2c153

3 files changed

Lines changed: 170 additions & 2 deletions

File tree

sentry_sdk/utils.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from copy import copy
1515
from datetime import datetime
1616
from decimal import Decimal
17-
from functools import partial, partialmethod
17+
from functools import partial, partialmethod, wraps
1818
from numbers import Real
1919
from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
2020

@@ -26,11 +26,14 @@
2626
BaseExceptionGroup = None # type: ignore
2727

2828
import sentry_sdk
29+
import sentry_sdk.hub
2930
from sentry_sdk._compat import PY37
3031
from sentry_sdk._types import TYPE_CHECKING
3132
from sentry_sdk.consts import DEFAULT_MAX_VALUE_LENGTH, EndpointType
3233

3334
if TYPE_CHECKING:
35+
from collections.abc import Awaitable
36+
3437
from types import FrameType, TracebackType
3538
from typing import (
3639
Any,
@@ -41,14 +44,20 @@
4144
List,
4245
NoReturn,
4346
Optional,
47+
ParamSpec,
4448
Set,
4549
Tuple,
4650
Type,
51+
TypeVar,
4752
Union,
4853
)
4954

55+
import sentry_sdk.integrations
5056
from sentry_sdk._types import Event, ExcInfo
5157

58+
P = ParamSpec("P")
59+
R = TypeVar("R")
60+
5261

5362
epoch = datetime(1970, 1, 1)
5463

@@ -1622,6 +1631,74 @@ def reraise(tp, value, tb=None):
16221631
raise value
16231632

16241633

1634+
def ensure_integration_enabled(
1635+
integration, # type: type[sentry_sdk.integrations.Integration]
1636+
original_function, # type: Callable[P, R]
1637+
):
1638+
# type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
1639+
"""
1640+
Ensures a given integration is enabled prior to calling a Sentry-patched function.
1641+
1642+
The function takes as its parameters the integration that must be enabled and the original
1643+
function that the SDK is patching. The function returns a function that takes the
1644+
decorated (Sentry-patched) function as its parameter, and returns a function that, when
1645+
called, checks whether the given integration is enabled. If the integration is enabled, the
1646+
function calls the decorated, Sentry-patched function. If the integration is not enabled,
1647+
the original function is called.
1648+
1649+
The function also takes care of preserving the original function's signature and docstring.
1650+
1651+
Example usage:
1652+
1653+
```python
1654+
@ensure_integration_enabled(MyIntegration, my_function)
1655+
def patch_my_function():
1656+
with sentry_sdk.start_transaction(...):
1657+
return my_function()
1658+
```
1659+
"""
1660+
1661+
def patcher(sentry_patched_function):
1662+
# type: (Callable[P, R]) -> Callable[P, R]
1663+
@wraps(original_function)
1664+
def runner(*args: "P.args", **kwargs: "P.kwargs"):
1665+
# type: (...) -> R
1666+
if sentry_sdk.get_client().get_integration(integration) is None:
1667+
return original_function(*args, **kwargs)
1668+
1669+
return sentry_patched_function(*args, **kwargs)
1670+
1671+
return runner
1672+
1673+
return patcher
1674+
1675+
1676+
def ensure_integration_enabled_async(
1677+
integration, # type: type[sentry_sdk.integrations.Integration]
1678+
original_function, # type: Callable[P, Awaitable[R]]
1679+
):
1680+
# type: (...) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]
1681+
"""
1682+
Version of `ensure_integration_enabled` for decorating async functions.
1683+
1684+
Please refer to the `ensure_integration_enabled` documentation for more information.
1685+
"""
1686+
1687+
def patcher(sentry_patched_function):
1688+
# type: (Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]
1689+
@wraps(original_function)
1690+
async def runner(*args: "P.args", **kwargs: "P.kwargs"):
1691+
# type: (...) -> R
1692+
if sentry_sdk.get_client().get_integration(integration) is None:
1693+
return await original_function(*args, **kwargs)
1694+
1695+
return await sentry_patched_function(*args, **kwargs)
1696+
1697+
return runner
1698+
1699+
return patcher
1700+
1701+
16251702
if PY37:
16261703

16271704
def nanosecond_time():

tests/integrations/cloud_resource_context/test_cloud_resource_context.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ def test_setup_once(
395395
fake_set_context.assert_not_called()
396396

397397
if warning_called:
398-
assert fake_warning.call_count == 1
398+
correct_warning_found = False
399+
for call in fake_warning.call_args_list:
400+
if call[0][0].startswith("Invalid value for cloud_provider:"):
401+
correct_warning_found = True
402+
break
403+
404+
assert correct_warning_found
399405
else:
400406
fake_warning.assert_not_called()

tests/test_utils.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66

77
import sentry_sdk
8+
from sentry_sdk.integrations import Integration
89
from sentry_sdk.utils import (
910
Components,
1011
Dsn,
@@ -21,9 +22,21 @@
2122
serialize_frame,
2223
is_sentry_url,
2324
_get_installed_modules,
25+
ensure_integration_enabled,
26+
ensure_integration_enabled_async,
2427
)
2528

2629

30+
class TestIntegration(Integration):
31+
"""
32+
Test integration for testing ensure_integration_enabled and
33+
ensure_integration_enabled_async decorators.
34+
"""
35+
36+
identifier = "test"
37+
setup_once = mock.MagicMock()
38+
39+
2740
def _normalize_distribution_name(name):
2841
# type: (str) -> str
2942
"""Normalize distribution name according to PEP-0503.
@@ -567,3 +580,75 @@ def test_default_release_empty_string():
567580
release = get_default_release()
568581

569582
assert release is None
583+
584+
585+
def test_ensure_integration_enabled_integration_enabled(sentry_init):
586+
def original_function():
587+
return "original"
588+
589+
def function_to_patch():
590+
return "patched"
591+
592+
sentry_init(integrations=[TestIntegration()])
593+
594+
# Test the decorator by applying to function_to_patch
595+
patched_function = ensure_integration_enabled(TestIntegration, original_function)(
596+
function_to_patch
597+
)
598+
599+
assert patched_function() == "patched"
600+
601+
602+
def test_ensure_integration_enabled_integration_disabled(sentry_init):
603+
def original_function():
604+
return "original"
605+
606+
def function_to_patch():
607+
return "patched"
608+
609+
sentry_init(integrations=[]) # TestIntegration is disabled
610+
611+
# Test the decorator by applying to function_to_patch
612+
patched_function = ensure_integration_enabled(TestIntegration, original_function)(
613+
function_to_patch
614+
)
615+
616+
assert patched_function() == "original"
617+
618+
619+
@pytest.mark.asyncio
620+
async def test_ensure_integration_enabled_async_integration_enabled(sentry_init):
621+
# Setup variables and functions for the test
622+
async def original_function():
623+
return "original"
624+
625+
async def function_to_patch():
626+
return "patched"
627+
628+
sentry_init(integrations=[TestIntegration()])
629+
630+
# Test the decorator by applying to function_to_patch
631+
patched_function = ensure_integration_enabled_async(
632+
TestIntegration, original_function
633+
)(function_to_patch)
634+
635+
assert await patched_function() == "patched"
636+
637+
638+
@pytest.mark.asyncio
639+
async def test_ensure_integration_enabled_async_integration_disabled(sentry_init):
640+
# Setup variables and functions for the test
641+
async def original_function():
642+
return "original"
643+
644+
async def function_to_patch():
645+
return "patched"
646+
647+
sentry_init(integrations=[]) # TestIntegration is disabled
648+
649+
# Test the decorator by applying to function_to_patch
650+
patched_function = ensure_integration_enabled_async(
651+
TestIntegration, original_function
652+
)(function_to_patch)
653+
654+
assert await patched_function() == "original"

0 commit comments

Comments
 (0)