From 1e9ba835df407f2f1ec2721c958bf89bf4e43c4a Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 30 Apr 2025 14:01:12 +0200 Subject: [PATCH 1/3] Added tests to show flags are not working. --- tests/integrations/fastapi/test_fastapi.py | 31 +++++++++++++++ tests/test_feature_flags.py | 45 ++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index 95838b1009..c599657a65 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -10,7 +10,9 @@ from fastapi.testclient import TestClient from fastapi.middleware.trustedhost import TrustedHostMiddleware +import sentry_sdk from sentry_sdk import capture_message +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations.asgi import SentryAsgiMiddleware from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.starlette import StarletteIntegration @@ -714,3 +716,32 @@ async def subapp_route(): assert event["transaction"] == "/subapp" else: assert event["transaction"].endswith("subapp_route") + + +@pytest.mark.asyncio +async def test_feature_flags(sentry_init, capture_events): + sentry_init(integrations=[StarletteIntegration(), FastApiIntegration()]) + + events = capture_events() + + app = FastAPI() + + @app.get("/error") + async def _error(): + add_feature_flag("hello", False) + + with sentry_sdk.start_span(name="test-span"): + with sentry_sdk.start_span(name="test-span-2"): + raise ValueError("something is wrong!") + + try: + client = TestClient(app) + client.get("/error") + except ValueError: + pass + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index 1b0ed13d49..3907ee93c1 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -31,6 +31,51 @@ def test_featureflags_integration(sentry_init, capture_events, uninstall_integra } +@pytest.mark.asyncio +async def test_featureflags_integration_spans_async(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + ) + events = capture_events() + + add_feature_flag("hello", False) + + try: + with sentry_sdk.start_span(name="test-span"): + with sentry_sdk.start_span(name="test-span-2"): + raise ValueError("something wrong!") + except ValueError as e: + sentry_sdk.capture_exception(e) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + + +def test_featureflags_integration_spans_sync(sentry_init, capture_events): + sentry_init( + traces_sample_rate=1.0, + ) + events = capture_events() + + add_feature_flag("hello", False) + + try: + with sentry_sdk.start_span(name="test-span"): + with sentry_sdk.start_span(name="test-span-2"): + raise ValueError("something wrong!") + except ValueError as e: + sentry_sdk.capture_exception(e) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + + def test_featureflags_integration_threaded( sentry_init, capture_events, uninstall_integration ): From 53607440f3b637fcd60022492651e9eff1436e48 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 5 May 2025 15:55:58 +0200 Subject: [PATCH 2/3] Feature Flags should life on the isolation scope --- sentry_sdk/feature_flags.py | 2 +- tests/integrations/fastapi/test_fastapi.py | 21 ++++++++++---- tests/test_feature_flags.py | 32 +++++++++++++++------- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index dd8d41c32e..eb53acae5d 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -64,7 +64,7 @@ def add_feature_flag(flag, result): Records a flag and its value to be sent on subsequent error events. We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. """ - flags = sentry_sdk.get_current_scope().flags + flags = sentry_sdk.get_isolation_scope().flags flags.set(flag, result) span = sentry_sdk.get_current_span() diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index c599657a65..3d79da92cc 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -720,7 +720,10 @@ async def subapp_route(): @pytest.mark.asyncio async def test_feature_flags(sentry_init, capture_events): - sentry_init(integrations=[StarletteIntegration(), FastApiIntegration()]) + sentry_init( + traces_sample_rate=1.0, + integrations=[StarletteIntegration(), FastApiIntegration()], + ) events = capture_events() @@ -740,8 +743,14 @@ async def _error(): except ValueError: pass - assert events[0]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - ] - } + found = False + for event in events: + if "exception" in event.keys(): + assert event["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + found = True + + assert found, "No event with exception found" diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index 3907ee93c1..e0ab1e254e 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -47,11 +47,17 @@ async def test_featureflags_integration_spans_async(sentry_init, capture_events) except ValueError as e: sentry_sdk.capture_exception(e) - assert events[0]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - ] - } + found = False + for event in events: + if "exception" in event.keys(): + assert event["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + found = True + + assert found, "No event with exception found" def test_featureflags_integration_spans_sync(sentry_init, capture_events): @@ -69,11 +75,17 @@ def test_featureflags_integration_spans_sync(sentry_init, capture_events): except ValueError as e: sentry_sdk.capture_exception(e) - assert events[0]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - ] - } + found = False + for event in events: + if "exception" in event.keys(): + assert event["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + found = True + + assert found, "No event with exception found" def test_featureflags_integration_threaded( From e635af799ba8a68baa380213fe2ccbc703708860 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Mon, 5 May 2025 16:46:45 +0200 Subject: [PATCH 3/3] Add `add_attachment()` top level API (#4364) Users should not need to get the Scope and add the attachment there, we can do it for them. This has been first [implemented in SDK 3.0](https://github.com/getsentry/sentry-python/pull/4360) and is now back ported to 2.x. --- docs/api.rst | 2 +- sentry_sdk/__init__.py | 1 + sentry_sdk/api.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 87c2535abd..a6fb49346d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -25,6 +25,7 @@ Capturing Data Enriching Events ================ +.. autofunction:: sentry_sdk.api.add_attachment .. autofunction:: sentry_sdk.api.add_breadcrumb .. autofunction:: sentry_sdk.api.set_context .. autofunction:: sentry_sdk.api.set_extra @@ -63,4 +64,3 @@ Managing Scope (advanced) .. autofunction:: sentry_sdk.api.push_scope .. autofunction:: sentry_sdk.api.new_scope - diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index b4859cc5d2..9fd7253fc2 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -15,6 +15,7 @@ "integrations", # From sentry_sdk.api "init", + "add_attachment", "add_breadcrumb", "capture_event", "capture_exception", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index a6b3c293dc..e56109cbd0 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -51,6 +51,7 @@ def overload(x): # When changing this, update __all__ in __init__.py too __all__ = [ "init", + "add_attachment", "add_breadcrumb", "capture_event", "capture_exception", @@ -184,6 +185,20 @@ def capture_exception( return get_current_scope().capture_exception(error, scope=scope, **scope_kwargs) +@scopemethod +def add_attachment( + bytes=None, # type: Union[None, bytes, Callable[[], bytes]] + filename=None, # type: Optional[str] + path=None, # type: Optional[str] + content_type=None, # type: Optional[str] + add_to_transactions=False, # type: bool +): + # type: (...) -> None + return get_isolation_scope().add_attachment( + bytes, filename, path, content_type, add_to_transactions + ) + + @scopemethod def add_breadcrumb( crumb=None, # type: Optional[Breadcrumb]