Skip to content

Commit bf48bd0

Browse files
authored
feat: Auto-enabling integrations behind feature flag (#625)
This was asked for in the context of APM where people would have to enable a lot of small integrations to get a meaningful span tree. Generally it's nice if people have to think about as little as possible (so, not about which integrations are necessary) when enabling the SDK. The semver compatibility is another thing. Yes, you could upgrade to a new version of the SDK and just get more integrations enabled automatically. Similar effects were already observable to some degree as we added more features to integrations that the user already explicitly enabled (breadcrumbs for django sql queries for example). I think we will just avoid this problem and make sure that new integrations or changes to existing ones don't break fundamental things like grouping, ever (like we already do)
1 parent 2c9a2b7 commit bf48bd0

File tree

16 files changed

+347
-112
lines changed

16 files changed

+347
-112
lines changed

docs-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
sphinx==2.3.1
22
sphinx-rtd-theme
33
sphinx-autodoc-typehints[type_comments]>=1.8.0
4+
typing-extensions

sentry_sdk/client.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ def _init_impl(self):
106106
self.integrations = setup_integrations(
107107
self.options["integrations"],
108108
with_defaults=self.options["default_integrations"],
109+
with_auto_enabling_integrations=self.options["_experiments"].get(
110+
"auto_enabling_integrations", False
111+
),
109112
)
110113
finally:
111114
_client_init_debug.set(old_debug)

sentry_sdk/consts.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,27 @@
99
from typing import Dict
1010
from typing import Any
1111
from typing import Sequence
12+
from typing_extensions import TypedDict
1213

1314
from sentry_sdk.transport import Transport
1415
from sentry_sdk.integrations import Integration
1516

1617
from sentry_sdk._types import Event, EventProcessor, BreadcrumbProcessor
1718

19+
# Experiments are feature flags to enable and disable certain unstable SDK
20+
# functionality. Changing them from the defaults (`None`) in production
21+
# code is highly discouraged. They are not subject to any stability
22+
# guarantees such as the ones from semantic versioning.
23+
Experiments = TypedDict(
24+
"Experiments",
25+
{
26+
"max_spans": Optional[int],
27+
"record_sql_params": Optional[bool],
28+
"auto_enabling_integrations": Optional[bool],
29+
},
30+
total=False,
31+
)
32+
1833

1934
# This type exists to trick mypy and PyCharm into thinking `init` and `Client`
2035
# take these arguments (even though they take opaque **kwargs)
@@ -49,7 +64,7 @@ def __init__(
4964
# DO NOT ENABLE THIS RIGHT NOW UNLESS YOU WANT TO EXCEED YOUR EVENT QUOTA IMMEDIATELY
5065
traces_sample_rate=0.0, # type: float
5166
traceparent_v2=False, # type: bool
52-
_experiments={}, # type: Dict[str, Any] # noqa: B006
67+
_experiments={}, # type: Experiments # noqa: B006
5368
):
5469
# type: (...) -> None
5570
pass

sentry_sdk/integrations/__init__.py

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,53 +9,85 @@
99
from sentry_sdk._types import MYPY
1010

1111
if MYPY:
12-
from typing import Iterator
12+
from typing import Callable
1313
from typing import Dict
14+
from typing import Iterator
1415
from typing import List
1516
from typing import Set
17+
from typing import Tuple
1618
from typing import Type
17-
from typing import Callable
1819

1920

2021
_installer_lock = Lock()
2122
_installed_integrations = set() # type: Set[str]
2223

2324

24-
def _generate_default_integrations_iterator(*import_strings):
25-
# type: (*str) -> Callable[[], Iterator[Type[Integration]]]
26-
def iter_default_integrations():
27-
# type: () -> Iterator[Type[Integration]]
25+
def _generate_default_integrations_iterator(integrations, auto_enabling_integrations):
26+
# type: (Tuple[str, ...], Tuple[str, ...]) -> Callable[[bool], Iterator[Type[Integration]]]
27+
28+
def iter_default_integrations(with_auto_enabling_integrations):
29+
# type: (bool) -> Iterator[Type[Integration]]
2830
"""Returns an iterator of the default integration classes:
2931
"""
3032
from importlib import import_module
3133

32-
for import_string in import_strings:
33-
module, cls = import_string.rsplit(".", 1)
34-
yield getattr(import_module(module), cls)
34+
if with_auto_enabling_integrations:
35+
all_import_strings = integrations + auto_enabling_integrations
36+
else:
37+
all_import_strings = integrations
38+
39+
for import_string in all_import_strings:
40+
try:
41+
module, cls = import_string.rsplit(".", 1)
42+
yield getattr(import_module(module), cls)
43+
except (DidNotEnable, SyntaxError) as e:
44+
logger.debug(
45+
"Did not import default integration %s: %s", import_string, e
46+
)
3547

3648
if isinstance(iter_default_integrations.__doc__, str):
37-
for import_string in import_strings:
49+
for import_string in integrations:
3850
iter_default_integrations.__doc__ += "\n- `{}`".format(import_string)
3951

4052
return iter_default_integrations
4153

4254

55+
_AUTO_ENABLING_INTEGRATIONS = (
56+
"sentry_sdk.integrations.django.DjangoIntegration",
57+
"sentry_sdk.integrations.flask.FlaskIntegration",
58+
"sentry_sdk.integrations.bottle.BottleIntegration",
59+
"sentry_sdk.integrations.falcon.FalconIntegration",
60+
"sentry_sdk.integrations.sanic.SanicIntegration",
61+
"sentry_sdk.integrations.celery.CeleryIntegration",
62+
"sentry_sdk.integrations.rq.RqIntegration",
63+
"sentry_sdk.integrations.aiohttp.AioHttpIntegration",
64+
"sentry_sdk.integrations.tornado.TornadoIntegration",
65+
"sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration",
66+
)
67+
68+
4369
iter_default_integrations = _generate_default_integrations_iterator(
44-
"sentry_sdk.integrations.logging.LoggingIntegration",
45-
"sentry_sdk.integrations.stdlib.StdlibIntegration",
46-
"sentry_sdk.integrations.excepthook.ExcepthookIntegration",
47-
"sentry_sdk.integrations.dedupe.DedupeIntegration",
48-
"sentry_sdk.integrations.atexit.AtexitIntegration",
49-
"sentry_sdk.integrations.modules.ModulesIntegration",
50-
"sentry_sdk.integrations.argv.ArgvIntegration",
51-
"sentry_sdk.integrations.threading.ThreadingIntegration",
70+
integrations=(
71+
# stdlib/base runtime integrations
72+
"sentry_sdk.integrations.logging.LoggingIntegration",
73+
"sentry_sdk.integrations.stdlib.StdlibIntegration",
74+
"sentry_sdk.integrations.excepthook.ExcepthookIntegration",
75+
"sentry_sdk.integrations.dedupe.DedupeIntegration",
76+
"sentry_sdk.integrations.atexit.AtexitIntegration",
77+
"sentry_sdk.integrations.modules.ModulesIntegration",
78+
"sentry_sdk.integrations.argv.ArgvIntegration",
79+
"sentry_sdk.integrations.threading.ThreadingIntegration",
80+
),
81+
auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS,
5282
)
5383

5484
del _generate_default_integrations_iterator
5585

5686

57-
def setup_integrations(integrations, with_defaults=True):
58-
# type: (List[Integration], bool) -> Dict[str, Integration]
87+
def setup_integrations(
88+
integrations, with_defaults=True, with_auto_enabling_integrations=False
89+
):
90+
# type: (List[Integration], bool, bool) -> Dict[str, Integration]
5991
"""Given a list of integration instances this installs them all. When
6092
`with_defaults` is set to `True` then all default integrations are added
6193
unless they were already provided before.
@@ -66,11 +98,17 @@ def setup_integrations(integrations, with_defaults=True):
6698

6799
logger.debug("Setting up integrations (with default = %s)", with_defaults)
68100

101+
# Integrations that are not explicitly set up by the user.
102+
used_as_default_integration = set()
103+
69104
if with_defaults:
70-
for integration_cls in iter_default_integrations():
105+
for integration_cls in iter_default_integrations(
106+
with_auto_enabling_integrations
107+
):
71108
if integration_cls.identifier not in integrations:
72109
instance = integration_cls()
73110
integrations[instance.identifier] = instance
111+
used_as_default_integration.add(instance.identifier)
74112

75113
for identifier, integration in iteritems(integrations):
76114
with _installer_lock:
@@ -90,6 +128,14 @@ def setup_integrations(integrations, with_defaults=True):
90128
integration.install()
91129
else:
92130
raise
131+
except DidNotEnable as e:
132+
if identifier not in used_as_default_integration:
133+
raise
134+
135+
logger.debug(
136+
"Did not enable default integration %s: %s", identifier, e
137+
)
138+
93139
_installed_integrations.add(identifier)
94140

95141
for identifier in integrations:
@@ -98,6 +144,16 @@ def setup_integrations(integrations, with_defaults=True):
98144
return integrations
99145

100146

147+
class DidNotEnable(Exception):
148+
"""
149+
The integration could not be enabled due to a trivial user error like
150+
`flask` not being installed for the `FlaskIntegration`.
151+
152+
This exception is silently swallowed for default integrations, but reraised
153+
for explicitly enabled integrations.
154+
"""
155+
156+
101157
class Integration(object):
102158
"""Baseclass for all integrations.
103159

sentry_sdk/integrations/aiohttp.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from sentry_sdk._compat import reraise
55
from sentry_sdk.hub import Hub
6-
from sentry_sdk.integrations import Integration
6+
from sentry_sdk.integrations import Integration, DidNotEnable
77
from sentry_sdk.integrations.logging import ignore_logger
88
from sentry_sdk.integrations._wsgi_common import (
99
_filter_headers,
@@ -18,8 +18,13 @@
1818
AnnotatedValue,
1919
)
2020

21-
import asyncio
22-
from aiohttp.web import Application, HTTPException, UrlDispatcher
21+
try:
22+
import asyncio
23+
24+
from aiohttp import __version__ as AIOHTTP_VERSION
25+
from aiohttp.web import Application, HTTPException, UrlDispatcher
26+
except ImportError:
27+
raise DidNotEnable("AIOHTTP not installed")
2328

2429
from sentry_sdk._types import MYPY
2530

@@ -43,6 +48,15 @@ class AioHttpIntegration(Integration):
4348
@staticmethod
4449
def setup_once():
4550
# type: () -> None
51+
52+
try:
53+
version = tuple(map(int, AIOHTTP_VERSION.split(".")))
54+
except (TypeError, ValueError):
55+
raise DidNotEnable("AIOHTTP version unparseable: {}".format(version))
56+
57+
if version < (3, 4):
58+
raise DidNotEnable("AIOHTTP 3.4 or newer required.")
59+
4660
if not HAS_REAL_CONTEXTVARS:
4761
# We better have contextvars or we're going to leak state between
4862
# requests.

sentry_sdk/integrations/bottle.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
event_from_exception,
77
transaction_from_function,
88
)
9-
from sentry_sdk.integrations import Integration
9+
from sentry_sdk.integrations import Integration, DidNotEnable
1010
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
1111
from sentry_sdk.integrations._wsgi_common import RequestExtractor
1212

@@ -22,7 +22,16 @@
2222

2323
from sentry_sdk._types import EventProcessor
2424

25-
from bottle import Bottle, Route, request as bottle_request, HTTPResponse
25+
try:
26+
from bottle import (
27+
Bottle,
28+
Route,
29+
request as bottle_request,
30+
HTTPResponse,
31+
__version__ as BOTTLE_VERSION,
32+
)
33+
except ImportError:
34+
raise DidNotEnable("Bottle not installed")
2635

2736

2837
class BottleIntegration(Integration):
@@ -32,6 +41,7 @@ class BottleIntegration(Integration):
3241

3342
def __init__(self, transaction_style="endpoint"):
3443
# type: (str) -> None
44+
3545
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
3646
if transaction_style not in TRANSACTION_STYLE_VALUES:
3747
raise ValueError(
@@ -44,6 +54,14 @@ def __init__(self, transaction_style="endpoint"):
4454
def setup_once():
4555
# type: () -> None
4656

57+
try:
58+
version = tuple(map(int, BOTTLE_VERSION.split(".")))
59+
except (TypeError, ValueError):
60+
raise DidNotEnable("Unparseable Bottle version: {}".format(version))
61+
62+
if version < (0, 12):
63+
raise DidNotEnable("Bottle 0.12 or newer required.")
64+
4765
# monkey patch method Bottle.__call__
4866
old_app = Bottle.__call__
4967

sentry_sdk/integrations/celery.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@
33
import functools
44
import sys
55

6-
from celery.exceptions import ( # type: ignore
7-
SoftTimeLimitExceeded,
8-
Retry,
9-
Ignore,
10-
Reject,
11-
)
12-
136
from sentry_sdk.hub import Hub
147
from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
158
from sentry_sdk.tracing import Span
169
from sentry_sdk._compat import reraise
17-
from sentry_sdk.integrations import Integration
10+
from sentry_sdk.integrations import Integration, DidNotEnable
1811
from sentry_sdk.integrations.logging import ignore_logger
1912
from sentry_sdk._types import MYPY
2013

@@ -29,6 +22,18 @@
2922
F = TypeVar("F", bound=Callable[..., Any])
3023

3124

25+
try:
26+
from celery import VERSION as CELERY_VERSION # type: ignore
27+
from celery.exceptions import ( # type: ignore
28+
SoftTimeLimitExceeded,
29+
Retry,
30+
Ignore,
31+
Reject,
32+
)
33+
except ImportError:
34+
raise DidNotEnable("Celery not installed")
35+
36+
3237
CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject)
3338

3439

@@ -42,6 +47,9 @@ def __init__(self, propagate_traces=True):
4247
@staticmethod
4348
def setup_once():
4449
# type: () -> None
50+
if CELERY_VERSION < (3,):
51+
raise DidNotEnable("Celery 3 or newer required.")
52+
4553
import celery.app.trace as trace # type: ignore
4654

4755
old_build_tracer = trace.build_tracer

0 commit comments

Comments
 (0)