From b2a68247e1a02b7e318c6efa0b82d467abb75efb Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:19:54 +0100 Subject: [PATCH 01/10] fix(quart): Support for quart_flask_patch --- sentry_sdk/integrations/flask.py | 10 +++++ tests/integrations/quart/test_quart.py | 58 +++++++++++++++++++++----- tox.ini | 1 + 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 45b4f0b2b1..687379193f 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -72,6 +72,16 @@ def __init__( @staticmethod def setup_once(): # type: () -> None + try: + from quart import Quart + + if Flask == Quart: + # This is Quart masquerading as Flask, don't enable the Flask + # integration. See https://github.com/getsentry/sentry-python/issues/2709 + raise DidNotEnable("Quart is impersonating Flask") + except ImportError: + pass + version = package_version("flask") _check_minimum_version(FlaskIntegration, version) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index f15b968ac5..cdd50f6d0e 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -13,22 +13,22 @@ from sentry_sdk.integrations.logging import LoggingIntegration import sentry_sdk.integrations.quart as quart_sentry -from quart import Quart, Response, abort, stream_with_context -from quart.views import View -from quart_auth import AuthUser, login_user - -try: - from quart_auth import QuartAuth +def quart_app_factory(): + # These imports are inlined because the test_quart_flask_patch testcase tests + # behavior that is triggered by importing a package before any Quart imports + # happen + from quart import Quart - auth_manager = QuartAuth() -except ImportError: - from quart_auth import AuthManager + try: + from quart_auth import QuartAuth - auth_manager = AuthManager() + auth_manager = QuartAuth() + except ImportError: + from quart_auth import AuthManager + auth_manager = AuthManager() -def quart_app_factory(): app = Quart(__name__) app.debug = False app.config["TESTING"] = False @@ -71,6 +71,34 @@ def integration_enabled_params(request): raise ValueError(request.param) +@pytest.mark.asyncio +@pytest.mark.forked +async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations): + # This testcase is forked because import quart_flask_patch needs to run + # before anything else Quart-related is imported (since it monkeypatches + # some things) and we don't want this to affect other testcases. + # + # It's also important that this testcase is run before any other testcase + # that uses quart_app_factory. + import quart_flask_patch # noqa: F401 + + app = quart_app_factory() + sentry_init( + integrations=[quart_sentry.QuartIntegration()], + ) + + @app.route("/") + async def index(): + return "ok" + + events = capture_events() + + client = app.test_client() + await client.get("/") + + assert not events + + @pytest.mark.asyncio async def test_has_context(sentry_init, capture_events): sentry_init(integrations=[quart_sentry.QuartIntegration()]) @@ -213,6 +241,8 @@ async def test_quart_auth_configured( monkeypatch, integration_enabled_params, ): + from quart_auth import AuthUser, login_user + sentry_init(send_default_pii=send_default_pii, **integration_enabled_params) app = quart_app_factory() @@ -368,6 +398,8 @@ async def error_handler(err): @pytest.mark.asyncio async def test_bad_request_not_captured(sentry_init, capture_events): + from quart import abort + sentry_init(integrations=[quart_sentry.QuartIntegration()]) app = quart_app_factory() events = capture_events() @@ -385,6 +417,8 @@ async def index(): @pytest.mark.asyncio async def test_does_not_leak_scope(sentry_init, capture_events): + from quart import Response, stream_with_context + sentry_init(integrations=[quart_sentry.QuartIntegration()]) app = quart_app_factory() events = capture_events() @@ -514,6 +548,8 @@ async def error(): @pytest.mark.asyncio async def test_class_based_views(sentry_init, capture_events): + from quart.views import View + sentry_init(integrations=[quart_sentry.QuartIntegration()]) app = quart_app_factory() events = capture_events() diff --git a/tox.ini b/tox.ini index 932ef256ab..56587c16d4 100644 --- a/tox.ini +++ b/tox.ini @@ -501,6 +501,7 @@ deps = # Quart quart: quart-auth quart: pytest-asyncio + quart: quart-flask-patch quart-v0.16: blinker<1.6 quart-v0.16: jinja2<3.1.0 quart-v0.16: Werkzeug<2.1.0 From efb1b8bde5011f2b612a058831c385a8cf751c99 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:24:23 +0100 Subject: [PATCH 02/10] also add to tox.jinja --- scripts/populate_tox/tox.jinja | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 9da986a35a..aee76670c5 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -384,6 +384,7 @@ deps = # Quart quart: quart-auth quart: pytest-asyncio + quart: quart-flask-patch quart-v0.16: blinker<1.6 quart-v0.16: jinja2<3.1.0 quart-v0.16: Werkzeug<2.1.0 From c2937947cb141ee2fe06d2e0ac96018dbd9b9953 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:26:54 +0100 Subject: [PATCH 03/10] wording, formatting --- tests/integrations/quart/test_quart.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index cdd50f6d0e..8b82f8d3dc 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -15,9 +15,9 @@ def quart_app_factory(): - # These imports are inlined because the test_quart_flask_patch testcase tests - # behavior that is triggered by importing a package before any Quart imports - # happen + # These imports are inlined because the `test_quart_flask_patch` testcase + # tests behavior that is triggered by importing a package before any Quart + # imports happen from quart import Quart try: @@ -74,12 +74,12 @@ def integration_enabled_params(request): @pytest.mark.asyncio @pytest.mark.forked async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations): - # This testcase is forked because import quart_flask_patch needs to run + # This testcase is forked because `import quart_flask_patch` needs to run # before anything else Quart-related is imported (since it monkeypatches # some things) and we don't want this to affect other testcases. # - # It's also important that this testcase is run before any other testcase - # that uses quart_app_factory. + # It's also important this testcase be run before any other testcases + # that uses `quart_app_factory`. import quart_flask_patch # noqa: F401 app = quart_app_factory() From b04d7aebc591ed4488d0dadb27069c29479eac69 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:27:22 +0100 Subject: [PATCH 04/10] typo --- tests/integrations/quart/test_quart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 8b82f8d3dc..06659905f5 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -78,7 +78,7 @@ async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations # before anything else Quart-related is imported (since it monkeypatches # some things) and we don't want this to affect other testcases. # - # It's also important this testcase be run before any other testcases + # It's also important this testcase be run before any other testcase # that uses `quart_app_factory`. import quart_flask_patch # noqa: F401 From 2782309639066d68aa1a837acba4b7980b98c9c9 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:31:42 +0100 Subject: [PATCH 05/10] add quart to reqs-linting for types --- requirements-linting.txt | 1 + requirements-testing.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-linting.txt b/requirements-linting.txt index 4255685b5e..0555373e7d 100644 --- a/requirements-linting.txt +++ b/requirements-linting.txt @@ -21,3 +21,4 @@ statsig UnleashClient typer strawberry-graphql +quart diff --git a/requirements-testing.txt b/requirements-testing.txt index 503ab5de68..cbc515eec2 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -14,4 +14,4 @@ socksio httpcore[http2] setuptools Brotli -docker \ No newline at end of file +docker From 31a0efde5e75f0da67772c7235d80747798c0b9a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:35:16 +0100 Subject: [PATCH 06/10] actually test an error to make sure we can capture --- tests/integrations/quart/test_quart.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 06659905f5..fbeaeca87d 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -89,14 +89,18 @@ async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations @app.route("/") async def index(): - return "ok" + 1 / 0 events = capture_events() client = app.test_client() - await client.get("/") + try: + await client.get("/") + except ZeroDivisionError: + pass - assert not events + (event,) = events + assert event["exception"]["values"][0]["mechanism"]["type"] == "quart" @pytest.mark.asyncio From a7aa1020a9836c44e8c5265aa5dff60281c48b8c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:39:48 +0100 Subject: [PATCH 07/10] actually lets not change this here --- requirements-linting.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-linting.txt b/requirements-linting.txt index 0555373e7d..4255685b5e 100644 --- a/requirements-linting.txt +++ b/requirements-linting.txt @@ -21,4 +21,3 @@ statsig UnleashClient typer strawberry-graphql -quart From 9a21230ee606674c1ce30fa0d9263dcc24f824c6 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:46:37 +0100 Subject: [PATCH 08/10] version restrict --- scripts/populate_tox/tox.jinja | 2 +- tests/integrations/quart/test_quart.py | 5 +++++ tox.ini | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index aee76670c5..5f1a26ac5e 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -384,7 +384,7 @@ deps = # Quart quart: quart-auth quart: pytest-asyncio - quart: quart-flask-patch + quart-{v0.19,latest}: quart-flask-patch quart-v0.16: blinker<1.6 quart-v0.16: jinja2<3.1.0 quart-v0.16: Werkzeug<2.1.0 diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index fbeaeca87d..9ebe33682c 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -1,3 +1,4 @@ +import importlib import json import threading from unittest import mock @@ -73,6 +74,10 @@ def integration_enabled_params(request): @pytest.mark.asyncio @pytest.mark.forked +@pytest.mark.skipif( + not importlib.util.find_spec("quart_flask_patch"), + reason="requires quart_flask_patch", +) async def test_quart_flask_patch(sentry_init, capture_events, reset_integrations): # This testcase is forked because `import quart_flask_patch` needs to run # before anything else Quart-related is imported (since it monkeypatches diff --git a/tox.ini b/tox.ini index 56587c16d4..2294fcc00b 100644 --- a/tox.ini +++ b/tox.ini @@ -501,7 +501,7 @@ deps = # Quart quart: quart-auth quart: pytest-asyncio - quart: quart-flask-patch + quart-{v0.19,latest}: quart-flask-patch quart-v0.16: blinker<1.6 quart-v0.16: jinja2<3.1.0 quart-v0.16: Werkzeug<2.1.0 From 281259b0592e47f96c1669f69ad0356b0b55a603 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 15:51:18 +0100 Subject: [PATCH 09/10] mypy; --- sentry_sdk/integrations/flask.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index 687379193f..ad0855b75a 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -73,7 +73,7 @@ def __init__( def setup_once(): # type: () -> None try: - from quart import Quart + from quart import Quart # type: ignore if Flask == Quart: # This is Quart masquerading as Flask, don't enable the Flask From a48d978f39a70c46be2f2dbbaea4cc4669cbc163 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 13 Mar 2025 16:00:31 +0100 Subject: [PATCH 10/10] wording --- sentry_sdk/integrations/flask.py | 4 +++- tests/integrations/quart/test_quart.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index ad0855b75a..f45ec6db20 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -78,7 +78,9 @@ def setup_once(): if Flask == Quart: # This is Quart masquerading as Flask, don't enable the Flask # integration. See https://github.com/getsentry/sentry-python/issues/2709 - raise DidNotEnable("Quart is impersonating Flask") + raise DidNotEnable( + "This is not a Flask app but rather Quart pretending to be Flask" + ) except ImportError: pass diff --git a/tests/integrations/quart/test_quart.py b/tests/integrations/quart/test_quart.py index 9ebe33682c..100642d245 100644 --- a/tests/integrations/quart/test_quart.py +++ b/tests/integrations/quart/test_quart.py @@ -18,7 +18,7 @@ def quart_app_factory(): # These imports are inlined because the `test_quart_flask_patch` testcase # tests behavior that is triggered by importing a package before any Quart - # imports happen + # imports happen, so we can't have these on the module level from quart import Quart try: