From bec9322a9136be8e63686d845f373ea1fd8db489 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 18 Feb 2020 11:36:26 +0100 Subject: [PATCH 01/13] feat: Auto-enabling integrations behind feature flag --- sentry_sdk/client.py | 3 + sentry_sdk/integrations/__init__.py | 96 +++++++++++++++++----- sentry_sdk/integrations/aiohttp.py | 20 ++++- sentry_sdk/integrations/bottle.py | 22 ++++- sentry_sdk/integrations/celery.py | 24 ++++-- sentry_sdk/integrations/django/__init__.py | 66 ++++++++------- sentry_sdk/integrations/falcon.py | 20 ++++- sentry_sdk/integrations/flask.py | 34 ++++++-- sentry_sdk/integrations/rq.py | 21 ++++- sentry_sdk/integrations/sanic.py | 25 ++++-- sentry_sdk/integrations/sqlalchemy.py | 20 ++++- sentry_sdk/integrations/tornado.py | 19 +++-- tests/integrations/flask/test_flask.py | 51 +++++++++--- tests/test_basics.py | 26 ++++++ 14 files changed, 337 insertions(+), 110 deletions(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index e83c8a02a0..57148205eb 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -106,6 +106,9 @@ def _init_impl(self): self.integrations = setup_integrations( self.options["integrations"], with_defaults=self.options["default_integrations"], + with_auto_enabling_integrations=self.options["_experiments"].get( + "auto_enabling_integrations" + ), ) finally: _client_init_debug.set(old_debug) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 18c8069e2f..4db3225640 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -9,53 +9,83 @@ from sentry_sdk._types import MYPY if MYPY: - from typing import Iterator + from typing import Callable from typing import Dict + from typing import Iterator from typing import List from typing import Set + from typing import Tuple from typing import Type - from typing import Callable _installer_lock = Lock() _installed_integrations = set() # type: Set[str] -def _generate_default_integrations_iterator(*import_strings): - # type: (*str) -> Callable[[], Iterator[Type[Integration]]] - def iter_default_integrations(): - # type: () -> Iterator[Type[Integration]] +def _generate_default_integrations_iterator(integrations, auto_enabling_integrations): + # type: (Tuple[str, ...], Tuple[str, ...]) -> Callable[[bool], Iterator[Type[Integration]]] + + def iter_default_integrations(with_auto_enabling_integrations): + # type: (bool) -> Iterator[Type[Integration]] """Returns an iterator of the default integration classes: """ from importlib import import_module - for import_string in import_strings: - module, cls = import_string.rsplit(".", 1) - yield getattr(import_module(module), cls) + if with_auto_enabling_integrations: + all_import_strings = integrations + auto_enabling_integrations + else: + all_import_strings = integrations + + for import_string in all_import_strings: + try: + module, cls = import_string.rsplit(".", 1) + yield getattr(import_module(module), cls) + except (DidNotEnable, SyntaxError) as e: + logger.debug( + "Did not import default integration %s: %s", import_string, e + ) if isinstance(iter_default_integrations.__doc__, str): - for import_string in import_strings: + for import_string in integrations: iter_default_integrations.__doc__ += "\n- `{}`".format(import_string) return iter_default_integrations iter_default_integrations = _generate_default_integrations_iterator( - "sentry_sdk.integrations.logging.LoggingIntegration", - "sentry_sdk.integrations.stdlib.StdlibIntegration", - "sentry_sdk.integrations.excepthook.ExcepthookIntegration", - "sentry_sdk.integrations.dedupe.DedupeIntegration", - "sentry_sdk.integrations.atexit.AtexitIntegration", - "sentry_sdk.integrations.modules.ModulesIntegration", - "sentry_sdk.integrations.argv.ArgvIntegration", - "sentry_sdk.integrations.threading.ThreadingIntegration", + integrations=( + # stdlib/base runtime integrations + "sentry_sdk.integrations.logging.LoggingIntegration", + "sentry_sdk.integrations.stdlib.StdlibIntegration", + "sentry_sdk.integrations.excepthook.ExcepthookIntegration", + "sentry_sdk.integrations.dedupe.DedupeIntegration", + "sentry_sdk.integrations.atexit.AtexitIntegration", + "sentry_sdk.integrations.modules.ModulesIntegration", + "sentry_sdk.integrations.argv.ArgvIntegration", + "sentry_sdk.integrations.threading.ThreadingIntegration", + ), + auto_enabling_integrations=( + # "auto-enabling" integrations + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + ), ) del _generate_default_integrations_iterator -def setup_integrations(integrations, with_defaults=True): - # type: (List[Integration], bool) -> Dict[str, Integration] +def setup_integrations( + integrations, with_defaults=True, with_auto_enabling_integrations=False +): + # type: (List[Integration], bool, bool) -> Dict[str, Integration] """Given a list of integration instances this installs them all. When `with_defaults` is set to `True` then all default integrations are added unless they were already provided before. @@ -66,11 +96,17 @@ def setup_integrations(integrations, with_defaults=True): logger.debug("Setting up integrations (with default = %s)", with_defaults) + # Integrations that are not explicitly set up by the user. + used_as_default_integration = set() + if with_defaults: - for integration_cls in iter_default_integrations(): + for integration_cls in iter_default_integrations( + with_auto_enabling_integrations + ): if integration_cls.identifier not in integrations: instance = integration_cls() integrations[instance.identifier] = instance + used_as_default_integration.add(instance.identifier) for identifier, integration in iteritems(integrations): with _installer_lock: @@ -90,6 +126,14 @@ def setup_integrations(integrations, with_defaults=True): integration.install() else: raise + except DidNotEnable as e: + if identifier not in used_as_default_integration: + raise + + logger.debug( + "Did not enable default integration %s: %s", identifier, e + ) + _installed_integrations.add(identifier) for identifier in integrations: @@ -98,6 +142,16 @@ def setup_integrations(integrations, with_defaults=True): return integrations +class DidNotEnable(Exception): + """ + The integration could not be enabled due to a trivial user error like + `flask` not being installed for the `FlaskIntegration`. + + This exception is silently swallowed for default integrations, but reraised + for explicitly enabled integrations. + """ + + class Integration(object): """Baseclass for all integrations. diff --git a/sentry_sdk/integrations/aiohttp.py b/sentry_sdk/integrations/aiohttp.py index 20b1a7145c..02c76df7ef 100644 --- a/sentry_sdk/integrations/aiohttp.py +++ b/sentry_sdk/integrations/aiohttp.py @@ -3,7 +3,7 @@ from sentry_sdk._compat import reraise from sentry_sdk.hub import Hub -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk.integrations._wsgi_common import ( _filter_headers, @@ -18,8 +18,13 @@ AnnotatedValue, ) -import asyncio -from aiohttp.web import Application, HTTPException, UrlDispatcher +try: + import asyncio + + from aiohttp import __version__ as AIOHTTP_VERSION + from aiohttp.web import Application, HTTPException, UrlDispatcher +except ImportError: + raise DidNotEnable("AIOHTTP not installed") from sentry_sdk._types import MYPY @@ -43,6 +48,15 @@ class AioHttpIntegration(Integration): @staticmethod def setup_once(): # type: () -> None + + try: + version = tuple(map(int, AIOHTTP_VERSION.split("."))) + except (TypeError, ValueError): + raise DidNotEnable("AIOHTTP version unparseable: {}".format(version)) + + if version < (3, 4): + raise DidNotEnable("AIOHTTP 3.4 or newer required.") + if not HAS_REAL_CONTEXTVARS: # We better have contextvars or we're going to leak state between # requests. diff --git a/sentry_sdk/integrations/bottle.py b/sentry_sdk/integrations/bottle.py index 93ca96ea34..8dab3757ea 100644 --- a/sentry_sdk/integrations/bottle.py +++ b/sentry_sdk/integrations/bottle.py @@ -6,7 +6,7 @@ event_from_exception, transaction_from_function, ) -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -22,7 +22,16 @@ from sentry_sdk._types import EventProcessor -from bottle import Bottle, Route, request as bottle_request, HTTPResponse +try: + from bottle import ( + Bottle, + Route, + request as bottle_request, + HTTPResponse, + __version__ as BOTTLE_VERSION, + ) +except ImportError: + raise DidNotEnable("Bottle not installed") class BottleIntegration(Integration): @@ -32,6 +41,7 @@ class BottleIntegration(Integration): def __init__(self, transaction_style="endpoint"): # type: (str) -> None + TRANSACTION_STYLE_VALUES = ("endpoint", "url") if transaction_style not in TRANSACTION_STYLE_VALUES: raise ValueError( @@ -44,6 +54,14 @@ def __init__(self, transaction_style="endpoint"): def setup_once(): # type: () -> None + try: + version = tuple(map(int, BOTTLE_VERSION.split("."))) + except (TypeError, ValueError): + raise DidNotEnable("Unparseable Bottle version: {}".format(version)) + + if version < (0, 12): + raise DidNotEnable("Bottle 0.12 or newer required.") + # monkey patch method Bottle.__call__ old_app = Bottle.__call__ diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 38c2452618..19ee9ae895 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -3,18 +3,11 @@ import functools import sys -from celery.exceptions import ( # type: ignore - SoftTimeLimitExceeded, - Retry, - Ignore, - Reject, -) - from sentry_sdk.hub import Hub from sentry_sdk.utils import capture_internal_exceptions, event_from_exception from sentry_sdk.tracing import Span from sentry_sdk._compat import reraise -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk._types import MYPY @@ -29,6 +22,18 @@ F = TypeVar("F", bound=Callable[..., Any]) +try: + from celery import VERSION + from celery.exceptions import ( # type: ignore + SoftTimeLimitExceeded, + Retry, + Ignore, + Reject, + ) +except ImportError: + raise DidNotEnable("Celery not installed") + + CELERY_CONTROL_FLOW_EXCEPTIONS = (Retry, Ignore, Reject) @@ -42,6 +47,9 @@ def __init__(self, propagate_traces=True): @staticmethod def setup_once(): # type: () -> None + if VERSION < (3,): + raise DidNotEnable("Celery 3 or newer required.") + import celery.app.trace as trace # type: ignore old_build_tracer = trace.build_tracer diff --git a/sentry_sdk/integrations/django/__init__.py b/sentry_sdk/integrations/django/__init__.py index 698516e6b3..ab252cb680 100644 --- a/sentry_sdk/integrations/django/__init__.py +++ b/sentry_sdk/integrations/django/__init__.py @@ -5,11 +5,40 @@ import threading import weakref -from django import VERSION as DJANGO_VERSION -from django.core import signals - from sentry_sdk._types import MYPY -from sentry_sdk.utils import HAS_REAL_CONTEXTVARS, logger +from sentry_sdk.hub import Hub, _should_send_default_pii +from sentry_sdk.scope import add_global_event_processor +from sentry_sdk.serializer import add_global_repr_processor +from sentry_sdk.tracing import record_sql_queries +from sentry_sdk.utils import ( + HAS_REAL_CONTEXTVARS, + logger, + capture_internal_exceptions, + event_from_exception, + transaction_from_function, + walk_exception_chain, +) +from sentry_sdk.integrations import Integration, DidNotEnable +from sentry_sdk.integrations.logging import ignore_logger +from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +from sentry_sdk.integrations._wsgi_common import RequestExtractor + +try: + from django import VERSION as DJANGO_VERSION + from django.core import signals + + try: + from django.urls import resolve + except ImportError: + from django.core.urlresolvers import resolve +except ImportError: + raise DidNotEnable("Django not installed") + + +from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER +from sentry_sdk.integrations.django.templates import get_template_frame_from_exception +from sentry_sdk.integrations.django.middleware import patch_django_middlewares + if MYPY: from typing import Any @@ -28,31 +57,6 @@ from sentry_sdk._types import Event, Hint, EventProcessor, NotImplementedType -try: - from django.urls import resolve -except ImportError: - from django.core.urlresolvers import resolve - -from sentry_sdk import Hub -from sentry_sdk.hub import _should_send_default_pii -from sentry_sdk.scope import add_global_event_processor -from sentry_sdk.serializer import add_global_repr_processor -from sentry_sdk.tracing import record_sql_queries -from sentry_sdk.utils import ( - capture_internal_exceptions, - event_from_exception, - transaction_from_function, - walk_exception_chain, -) -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations.logging import ignore_logger -from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware -from sentry_sdk.integrations._wsgi_common import RequestExtractor -from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER -from sentry_sdk.integrations.django.templates import get_template_frame_from_exception -from sentry_sdk.integrations.django.middleware import patch_django_middlewares - - if DJANGO_VERSION < (1, 10): def is_authenticated(request_user): @@ -87,6 +91,10 @@ def __init__(self, transaction_style="url", middleware_spans=True): @staticmethod def setup_once(): # type: () -> None + + if DJANGO_VERSION < (1, 6): + raise DidNotEnable("Django 1.6 or newer is required.") + install_sql_hook() # Patch in our custom middleware. diff --git a/sentry_sdk/integrations/falcon.py b/sentry_sdk/integrations/falcon.py index bf644b99c4..07f4098ef6 100644 --- a/sentry_sdk/integrations/falcon.py +++ b/sentry_sdk/integrations/falcon.py @@ -1,9 +1,7 @@ from __future__ import absolute_import -import falcon # type: ignore -import falcon.api_helpers # type: ignore from sentry_sdk.hub import Hub -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import RequestExtractor from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.utils import capture_internal_exceptions, event_from_exception @@ -17,6 +15,14 @@ from sentry_sdk._types import EventProcessor +try: + import falcon # type: ignore + import falcon.api_helpers # type: ignore + + from falcon import __version__ as FALCON_VERSION +except ImportError: + raise DidNotEnable("Falcon not installed") + class FalconRequestExtractor(RequestExtractor): def env(self): @@ -93,6 +99,14 @@ def __init__(self, transaction_style="uri_template"): @staticmethod def setup_once(): # type: () -> None + try: + version = tuple(map(int, FALCON_VERSION.split("."))) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable Falcon version: {}".format(FALCON_VERSION)) + + if version < (1, 4): + raise DidNotEnable("Falcon 1.4 or newer required.") + _patch_wsgi_app() _patch_handle_exception() _patch_prepare_middleware() diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 8f2612eba2..6031c1b621 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -4,7 +4,7 @@ from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations._wsgi_common import RequestExtractor @@ -22,18 +22,28 @@ from sentry_sdk._types import EventProcessor + try: import flask_login # type: ignore except ImportError: flask_login = None -from flask import Request, Flask, _request_ctx_stack, _app_ctx_stack # type: ignore -from flask.signals import ( - appcontext_pushed, - appcontext_tearing_down, - got_request_exception, - request_started, -) +try: + from flask import ( # type: ignore + Request, + Flask, + _request_ctx_stack, + _app_ctx_stack, + __version__ as FLASK_VERSION, + ) + from flask.signals import ( + appcontext_pushed, + appcontext_tearing_down, + got_request_exception, + request_started, + ) +except ImportError: + raise DidNotEnable("Flask is not installed") class FlaskIntegration(Integration): @@ -54,6 +64,14 @@ def __init__(self, transaction_style="endpoint"): @staticmethod def setup_once(): # type: () -> None + try: + version = tuple(map(int, FLASK_VERSION.split(".")[:3])) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable Flask version: {}".format(FLASK_VERSION)) + + if version < (0, 11): + raise DidNotEnable("Flask 0.11 or newer is required.") + appcontext_pushed.connect(_push_appctx) appcontext_tearing_down.connect(_pop_appctx) request_started.connect(_request_started) diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index f34afeb93e..fbe8cdda3d 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -3,13 +3,18 @@ import weakref from sentry_sdk.hub import Hub -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.tracing import Span from sentry_sdk.utils import capture_internal_exceptions, event_from_exception -from rq.timeouts import JobTimeoutException -from rq.worker import Worker -from rq.queue import Queue + +try: + from rq.version import VERSION as RQ_VERSION + from rq.timeouts import JobTimeoutException + from rq.worker import Worker + from rq.queue import Queue +except ImportError: + raise DidNotEnable("RQ not installed") from sentry_sdk._types import MYPY @@ -31,6 +36,14 @@ class RqIntegration(Integration): def setup_once(): # type: () -> None + try: + version = tuple(map(int, RQ_VERSION.split(".")[:3])) + except (ValueError, TypeError): + raise DidNotEnable("Unparseable RQ version: {}".format(RQ_VERSION)) + + if version < (0, 6): + raise DidNotEnable("RQ 0.6 or newer is required.") + old_perform_job = Worker.perform_job def sentry_patched_perform_job(self, job, *args, **kwargs): diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 301685443e..92e1e418a9 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -9,15 +9,10 @@ event_from_exception, HAS_REAL_CONTEXTVARS, ) -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import RequestExtractor, _filter_headers from sentry_sdk.integrations.logging import ignore_logger -from sanic import Sanic, __version__ as VERSION -from sanic.exceptions import SanicException -from sanic.router import Router -from sanic.handlers import ErrorHandler - from sentry_sdk._types import MYPY if MYPY: @@ -32,6 +27,14 @@ from sentry_sdk._types import Event, EventProcessor, Hint +try: + from sanic import Sanic, __version__ as VERSION + from sanic.exceptions import SanicException + from sanic.router import Router + from sanic.handlers import ErrorHandler +except ImportError: + raise DidNotEnable("Sanic not installed") + class SanicIntegration(Integration): identifier = "sanic" @@ -39,10 +42,18 @@ class SanicIntegration(Integration): @staticmethod def setup_once(): # type: () -> None + try: + version = tuple(map(int, VERSION)) + except (TypeError, ValueError): + raise DidNotEnable("Unparseable Sanic version: {}".format(version)) + + if version < (0, 8): + raise DidNotEnable("Sanic 0.8 or newer required.") + if not HAS_REAL_CONTEXTVARS: # We better have contextvars or we're going to leak state between # requests. - raise RuntimeError( + raise DidNotEnable( "The sanic integration for Sentry requires Python 3.7+ " " or aiocontextvars package" ) diff --git a/sentry_sdk/integrations/sqlalchemy.py b/sentry_sdk/integrations/sqlalchemy.py index 5ce2a02c10..39525fe632 100644 --- a/sentry_sdk/integrations/sqlalchemy.py +++ b/sentry_sdk/integrations/sqlalchemy.py @@ -2,11 +2,15 @@ from sentry_sdk._types import MYPY from sentry_sdk.hub import Hub -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.tracing import record_sql_queries -from sqlalchemy.engine import Engine # type: ignore -from sqlalchemy.event import listen # type: ignore +try: + from sqlalchemy.engine import Engine # type: ignore + from sqlalchemy.event import listen # type: ignore + from sqlalchemy import __version__ as SQLALCHEMY_VERSION +except ImportError: + raise DidNotEnable("SQLAlchemy not installed.") if MYPY: from typing import Any @@ -23,6 +27,16 @@ class SqlalchemyIntegration(Integration): def setup_once(): # type: () -> None + try: + version = tuple(map(int, SQLALCHEMY_VERSION.split("b")[0].split("."))) + except (TypeError, ValueError): + raise DidNotEnable( + "Unparseable SQLAlchemy version: {}".format(SQLALCHEMY_VERSION) + ) + + if version < (1, 2): + raise DidNotEnable("SQLAlchemy 1.2 or newer required.") + listen(Engine, "before_cursor_execute", _before_cursor_execute) listen(Engine, "after_cursor_execute", _after_cursor_execute) listen(Engine, "handle_error", _handle_error) diff --git a/sentry_sdk/integrations/tornado.py b/sentry_sdk/integrations/tornado.py index 3c43e0180c..abd540b611 100644 --- a/sentry_sdk/integrations/tornado.py +++ b/sentry_sdk/integrations/tornado.py @@ -8,7 +8,7 @@ capture_internal_exceptions, transaction_from_function, ) -from sentry_sdk.integrations import Integration +from sentry_sdk.integrations import Integration, DidNotEnable from sentry_sdk.integrations._wsgi_common import ( RequestExtractor, _filter_headers, @@ -17,8 +17,12 @@ from sentry_sdk.integrations.logging import ignore_logger from sentry_sdk._compat import iteritems -from tornado.web import RequestHandler, HTTPError -from tornado.gen import coroutine +try: + from tornado import version_info as TORNADO_VERSION + from tornado.web import RequestHandler, HTTPError + from tornado.gen import coroutine +except ImportError: + raise DidNotEnable("Tornado not installed") from sentry_sdk._types import MYPY @@ -37,16 +41,13 @@ class TornadoIntegration(Integration): @staticmethod def setup_once(): # type: () -> None - import tornado - - tornado_version = getattr(tornado, "version_info", None) - if tornado_version is None or tornado_version < (5, 0): - raise RuntimeError("Tornado 5+ required") + if TORNADO_VERSION < (5, 0): + raise DidNotEnable("Tornado 5+ required") if not HAS_REAL_CONTEXTVARS: # Tornado is async. We better have contextvars or we're going to leak # state between requests. - raise RuntimeError( + raise DidNotEnable( "The tornado integration for Sentry requires Python 3.6+ or the aiocontextvars package" ) diff --git a/tests/integrations/flask/test_flask.py b/tests/integrations/flask/test_flask.py index a184fec577..69e5f5a4a8 100644 --- a/tests/integrations/flask/test_flask.py +++ b/tests/integrations/flask/test_flask.py @@ -39,6 +39,16 @@ def hi(): return app +@pytest.fixture(params=("auto", "manual")) +def integration_enabled_params(request): + if request.param == "auto": + return {"_experiments": {"auto_enabling_integrations": True}} + elif request.param == "manual": + return {"integrations": [flask_sentry.FlaskIntegration()]} + else: + raise ValueError(request.param) + + def test_has_context(sentry_init, app, capture_events): sentry_init(integrations=[flask_sentry.FlaskIntegration()]) events = capture_events() @@ -76,8 +86,16 @@ def test_transaction_style( @pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("testing", (True, False)) -def test_errors(sentry_init, capture_exceptions, capture_events, app, debug, testing): - sentry_init(integrations=[flask_sentry.FlaskIntegration()], debug=True) +def test_errors( + sentry_init, + capture_exceptions, + capture_events, + app, + debug, + testing, + integration_enabled_params, +): + sentry_init(debug=True, **integration_enabled_params) app.debug = debug app.testing = testing @@ -102,8 +120,10 @@ def index(): assert event["exception"]["values"][0]["mechanism"]["type"] == "flask" -def test_flask_login_not_installed(sentry_init, app, capture_events, monkeypatch): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) +def test_flask_login_not_installed( + sentry_init, app, capture_events, monkeypatch, integration_enabled_params +): + sentry_init(**integration_enabled_params) monkeypatch.setattr(flask_sentry, "flask_login", None) @@ -116,8 +136,10 @@ def test_flask_login_not_installed(sentry_init, app, capture_events, monkeypatch assert event.get("user", {}).get("id") is None -def test_flask_login_not_configured(sentry_init, app, capture_events, monkeypatch): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) +def test_flask_login_not_configured( + sentry_init, app, capture_events, monkeypatch, integration_enabled_params +): + sentry_init(**integration_enabled_params) assert flask_sentry.flask_login @@ -130,9 +152,9 @@ def test_flask_login_not_configured(sentry_init, app, capture_events, monkeypatc def test_flask_login_partially_configured( - sentry_init, app, capture_events, monkeypatch + sentry_init, app, capture_events, monkeypatch, integration_enabled_params ): - sentry_init(integrations=[flask_sentry.FlaskIntegration()]) + sentry_init(**integration_enabled_params) events = capture_events() @@ -149,12 +171,15 @@ def test_flask_login_partially_configured( @pytest.mark.parametrize("send_default_pii", [True, False]) @pytest.mark.parametrize("user_id", [None, "42", 3]) def test_flask_login_configured( - send_default_pii, sentry_init, app, user_id, capture_events, monkeypatch + send_default_pii, + sentry_init, + app, + user_id, + capture_events, + monkeypatch, + integration_enabled_params, ): - sentry_init( - send_default_pii=send_default_pii, - integrations=[flask_sentry.FlaskIntegration()], - ) + sentry_init(send_default_pii=send_default_pii, **integration_enabled_params) class User(object): is_authenticated = is_active = True diff --git a/tests/test_basics.py b/tests/test_basics.py index 421c6491b7..f9fd456cf6 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -12,6 +12,7 @@ last_event_id, Hub, ) + from sentry_sdk.integrations.logging import LoggingIntegration @@ -37,6 +38,31 @@ def error_processor(event, exc_info): assert event["exception"]["values"][0]["value"] == "aha! whatever" +def test_auto_enabling_integrations_does_not_break(sentry_init, caplog): + caplog.set_level(logging.DEBUG) + + sentry_init(_experiments={"auto_enabling_integrations": True}, debug=True) + + for import_string in ( + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", + ): + assert any( + record.message.startswith( + "Did not import default integration {}:".format(import_string) + ) + for record in caplog.records + ) + + def test_event_id(sentry_init, capture_events): sentry_init() events = capture_events() From b2619b7e1da8a25bfd3dace7c676c30d144966d9 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 18 Feb 2020 23:49:46 +0100 Subject: [PATCH 02/13] fix: Fix sanic tests --- sentry_sdk/integrations/sanic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 92e1e418a9..0cd10ab6fd 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -43,9 +43,9 @@ class SanicIntegration(Integration): def setup_once(): # type: () -> None try: - version = tuple(map(int, VERSION)) + version = tuple(map(int, VERSION.split("."))) except (TypeError, ValueError): - raise DidNotEnable("Unparseable Sanic version: {}".format(version)) + raise DidNotEnable("Unparseable Sanic version: {}".format(VERSION)) if version < (0, 8): raise DidNotEnable("Sanic 0.8 or newer required.") From e68319706bdc8f8df604c1612bb43ca12c524a6d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 13:23:19 +0100 Subject: [PATCH 03/13] fix: Fix linters --- sentry_sdk/integrations/celery.py | 2 +- sentry_sdk/integrations/sqlalchemy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 19ee9ae895..5f6127927a 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -23,7 +23,7 @@ try: - from celery import VERSION + from celery import VERSION # type: ignore from celery.exceptions import ( # type: ignore SoftTimeLimitExceeded, Retry, diff --git a/sentry_sdk/integrations/sqlalchemy.py b/sentry_sdk/integrations/sqlalchemy.py index 39525fe632..f24d2f20bf 100644 --- a/sentry_sdk/integrations/sqlalchemy.py +++ b/sentry_sdk/integrations/sqlalchemy.py @@ -8,7 +8,7 @@ try: from sqlalchemy.engine import Engine # type: ignore from sqlalchemy.event import listen # type: ignore - from sqlalchemy import __version__ as SQLALCHEMY_VERSION + from sqlalchemy import __version__ as SQLALCHEMY_VERSION # type: ignore except ImportError: raise DidNotEnable("SQLAlchemy not installed.") From e0b164b463d72dd443b7ffbd6641d51accb34dc4 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:26:34 +0100 Subject: [PATCH 04/13] Update sentry_sdk/integrations/sanic.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/integrations/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 0cd10ab6fd..8d08e0f6cb 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -43,7 +43,7 @@ class SanicIntegration(Integration): def setup_once(): # type: () -> None try: - version = tuple(map(int, VERSION.split("."))) + version = tuple(map(int, SANIC_VERSION.split("."))) except (TypeError, ValueError): raise DidNotEnable("Unparseable Sanic version: {}".format(VERSION)) From 8a14ac45d70238ed5a977f62c4832c744db91b1c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:26:57 +0100 Subject: [PATCH 05/13] Update sentry_sdk/client.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 57148205eb..200274fc1b 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -107,7 +107,7 @@ def _init_impl(self): self.options["integrations"], with_defaults=self.options["default_integrations"], with_auto_enabling_integrations=self.options["_experiments"].get( - "auto_enabling_integrations" + "auto_enabling_integrations", False ), ) finally: From 0c578f956749757d36d33e7622aa0095de01c74d Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:27:04 +0100 Subject: [PATCH 06/13] Update sentry_sdk/integrations/sanic.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/integrations/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 8d08e0f6cb..fb1a6de9cd 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -28,7 +28,7 @@ from sentry_sdk._types import Event, EventProcessor, Hint try: - from sanic import Sanic, __version__ as VERSION + from sanic import Sanic, __version__ as SANIC_VERSION from sanic.exceptions import SanicException from sanic.router import Router from sanic.handlers import ErrorHandler From 063180b9d741d17aee6dd94152c1fb971b15d7d1 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:27:15 +0100 Subject: [PATCH 07/13] Update sentry_sdk/integrations/sanic.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/integrations/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index fb1a6de9cd..012de12465 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -45,7 +45,7 @@ def setup_once(): try: version = tuple(map(int, SANIC_VERSION.split("."))) except (TypeError, ValueError): - raise DidNotEnable("Unparseable Sanic version: {}".format(VERSION)) + raise DidNotEnable("Unparseable Sanic version: {}".format(SANIC_VERSION)) if version < (0, 8): raise DidNotEnable("Sanic 0.8 or newer required.") From be677b4727c00f72d266286bdaae39efe69739c9 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:36:24 +0100 Subject: [PATCH 08/13] Update sentry_sdk/integrations/celery.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/integrations/celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 5f6127927a..5e878fc2ce 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -23,7 +23,7 @@ try: - from celery import VERSION # type: ignore + from celery import VERSION as CELERY_VERSION # type: ignore from celery.exceptions import ( # type: ignore SoftTimeLimitExceeded, Retry, From 6fa283fcd8fb66173ce45476e55efb0b18540a5a Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Wed, 19 Feb 2020 18:36:31 +0100 Subject: [PATCH 09/13] Update sentry_sdk/integrations/celery.py Co-Authored-By: Rodolfo Carvalho --- sentry_sdk/integrations/celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 5e878fc2ce..9b58796173 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -47,7 +47,7 @@ def __init__(self, propagate_traces=True): @staticmethod def setup_once(): # type: () -> None - if VERSION < (3,): + if CELERY_VERSION < (3,): raise DidNotEnable("Celery 3 or newer required.") import celery.app.trace as trace # type: ignore From 3d4a7559a4749d3cf2379167f70496a03c308e72 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Thu, 20 Feb 2020 11:26:39 +0100 Subject: [PATCH 10/13] fix: Rename all VERSIONs to SANIC_VERSION --- sentry_sdk/integrations/sanic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/sanic.py b/sentry_sdk/integrations/sanic.py index 012de12465..e8fdca422a 100644 --- a/sentry_sdk/integrations/sanic.py +++ b/sentry_sdk/integrations/sanic.py @@ -58,7 +58,7 @@ def setup_once(): " or aiocontextvars package" ) - if VERSION.startswith("0.8."): + if SANIC_VERSION.startswith("0.8."): # Sanic 0.8 and older creates a logger named "root" and puts a # stringified version of every exception in there (without exc_info), # which our error deduplication can't detect. From df973cc919691c55bc2503288a26c1835054078e Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 23 Feb 2020 17:37:18 +0100 Subject: [PATCH 11/13] doc: Document experiments in code --- sentry_sdk/consts.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 06591004a4..8969d9d38c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -9,12 +9,27 @@ from typing import Dict from typing import Any from typing import Sequence + from mypy_extensions import TypedDict from sentry_sdk.transport import Transport from sentry_sdk.integrations import Integration from sentry_sdk._types import Event, EventProcessor, BreadcrumbProcessor + # Experiments are feature flags to enable and disable certain unstable SDK + # functionality. Changing them from the defaults (`None`) in production + # code is highly discouraged. They are not subject to any stability + # guarantees such as the ones from semantic versioning. + Experiments = TypedDict( + "Experiments", + { + "max_spans": Optional[int], + "record_sql_params": Optional[bool], + "auto_enabling_integrations": Optional[bool], + }, + total=False, + ) + # This type exists to trick mypy and PyCharm into thinking `init` and `Client` # take these arguments (even though they take opaque **kwargs) @@ -49,7 +64,7 @@ def __init__( # DO NOT ENABLE THIS RIGHT NOW UNLESS YOU WANT TO EXCEED YOUR EVENT QUOTA IMMEDIATELY traces_sample_rate=0.0, # type: float traceparent_v2=False, # type: bool - _experiments={}, # type: Dict[str, Any] # noqa: B006 + _experiments={}, # type: Experiments # noqa: B006 ): # type: (...) -> None pass From cbd57707d93b78883017ba329765740b823eaf68 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 23 Feb 2020 17:41:09 +0100 Subject: [PATCH 12/13] ref: Extract sequence --- sentry_sdk/integrations/__init__.py | 28 +++++++++++++++------------- tests/test_basics.py | 16 +++------------- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index 4db3225640..f264bc4855 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -52,6 +52,20 @@ def iter_default_integrations(with_auto_enabling_integrations): return iter_default_integrations +_AUTO_ENABLING_INTEGRATIONS = ( + "sentry_sdk.integrations.django.DjangoIntegration", + "sentry_sdk.integrations.flask.FlaskIntegration", + "sentry_sdk.integrations.bottle.BottleIntegration", + "sentry_sdk.integrations.falcon.FalconIntegration", + "sentry_sdk.integrations.sanic.SanicIntegration", + "sentry_sdk.integrations.celery.CeleryIntegration", + "sentry_sdk.integrations.rq.RqIntegration", + "sentry_sdk.integrations.aiohttp.AioHttpIntegration", + "sentry_sdk.integrations.tornado.TornadoIntegration", + "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", +) + + iter_default_integrations = _generate_default_integrations_iterator( integrations=( # stdlib/base runtime integrations @@ -64,19 +78,7 @@ def iter_default_integrations(with_auto_enabling_integrations): "sentry_sdk.integrations.argv.ArgvIntegration", "sentry_sdk.integrations.threading.ThreadingIntegration", ), - auto_enabling_integrations=( - # "auto-enabling" integrations - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - ), + auto_enabling_integrations=_AUTO_ENABLING_INTEGRATIONS, ) del _generate_default_integrations_iterator diff --git a/tests/test_basics.py b/tests/test_basics.py index 0c184e2783..8953dc8803 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -13,6 +13,7 @@ Hub, ) +from sentry_sdk.integrations import _AUTO_ENABLING_INTEGRATIONS from sentry_sdk.integrations.logging import LoggingIntegration @@ -38,23 +39,12 @@ def error_processor(event, exc_info): assert event["exception"]["values"][0]["value"] == "aha! whatever" -def test_auto_enabling_integrations_does_not_break(sentry_init, caplog): +def test_auto_enabling_integrations_catches_import_error(sentry_init, caplog): caplog.set_level(logging.DEBUG) sentry_init(_experiments={"auto_enabling_integrations": True}, debug=True) - for import_string in ( - "sentry_sdk.integrations.django.DjangoIntegration", - "sentry_sdk.integrations.flask.FlaskIntegration", - "sentry_sdk.integrations.bottle.BottleIntegration", - "sentry_sdk.integrations.falcon.FalconIntegration", - "sentry_sdk.integrations.sanic.SanicIntegration", - "sentry_sdk.integrations.celery.CeleryIntegration", - "sentry_sdk.integrations.rq.RqIntegration", - "sentry_sdk.integrations.aiohttp.AioHttpIntegration", - "sentry_sdk.integrations.tornado.TornadoIntegration", - "sentry_sdk.integrations.sqlalchemy.SqlalchemyIntegration", - ): + for import_string in _AUTO_ENABLING_INTEGRATIONS: assert any( record.message.startswith( "Did not import default integration {}:".format(import_string) From f828a5660f893e77c58c3e7e205342b0b754de43 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 24 Feb 2020 17:53:46 +0100 Subject: [PATCH 13/13] docs: add typing extensions to doc requirements --- docs-requirements.txt | 1 + sentry_sdk/consts.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 8e52786424..78b98c5047 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,3 +1,4 @@ sphinx==2.3.1 sphinx-rtd-theme sphinx-autodoc-typehints[type_comments]>=1.8.0 +typing-extensions diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 8969d9d38c..30d140ffb1 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -9,7 +9,7 @@ from typing import Dict from typing import Any from typing import Sequence - from mypy_extensions import TypedDict + from typing_extensions import TypedDict from sentry_sdk.transport import Transport from sentry_sdk.integrations import Integration