From 3652ce3d1200fdbb96d0869e28d7cf7efdbd81ce Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:38:52 -0800 Subject: [PATCH 01/20] Add new integration and unit tests --- sentry_sdk/integrations/featureflags.py | 18 +++++ tests/integrations/featureflags/__init__.py | 0 .../featureflags/test_featureflags.py | 73 +++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 sentry_sdk/integrations/featureflags.py create mode 100644 tests/integrations/featureflags/__init__.py create mode 100644 tests/integrations/featureflags/test_featureflags.py diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py new file mode 100644 index 0000000000..8a1e6b4106 --- /dev/null +++ b/sentry_sdk/integrations/featureflags.py @@ -0,0 +1,18 @@ +from sentry_sdk.flag_utils import flag_error_processor + +import sentry_sdk +from sentry_sdk.integrations import Integration + + +class FeatureFlagsIntegration(Integration): + identifier = "featureflags" + + @staticmethod + def setup_once(): + scope = sentry_sdk.get_current_scope() + scope.add_error_processor(flag_error_processor) + + @staticmethod + def set_flag(flag: str, result: bool): + flags = sentry_sdk.get_current_scope().flags + flags.set(flag, result) diff --git a/tests/integrations/featureflags/__init__.py b/tests/integrations/featureflags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py new file mode 100644 index 0000000000..f89d64e9b3 --- /dev/null +++ b/tests/integrations/featureflags/test_featureflags.py @@ -0,0 +1,73 @@ +import asyncio +import concurrent.futures as cf + +import sentry_sdk +from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration + + +def test_featureflags_integration(sentry_init): + sentry_init(integrations=[FeatureFlagsIntegration()]) + flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) + + flags_integration.set_flag("hello", False) + flags_integration.set_flag("world", True) + flags_integration.set_flag("other", False) + + assert sentry_sdk.get_current_scope().flags.get() == [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": True}, + {"flag": "other", "result": False}, + ] + + +def test_featureflags_integration_threaded(sentry_init): + sentry_init(integrations=[FeatureFlagsIntegration()]) + flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) + + def task(flag_key): + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. + with sentry_sdk.isolation_scope(): + flags_integration.set_flag(flag_key, False) + return sentry_sdk.get_current_scope().flags.get() + + # Capture an eval before we split isolation scopes. + flags_integration.set_flag("hello", False) + + with cf.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(task, ["world", "other"])) + + assert results[0] == [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + + +def test_featureflags_integration_asyncio(sentry_init): + """Assert concurrently evaluated flags do not pollute one another.""" + sentry_init(integrations=[FeatureFlagsIntegration()]) + flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) + + async def task(flag_key): + with sentry_sdk.isolation_scope(): + flags_integration.set_flag(flag_key, False) + return sentry_sdk.get_current_scope().flags.get() + + async def runner(): + return asyncio.gather(task("world"), task("other")) + + flags_integration.set_flag("hello", False) + + results = asyncio.run(runner()).result() + assert results[0] == [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] From 625969e0dbc226c8f0cebac8bfc5eba374455817 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:43:40 -0800 Subject: [PATCH 02/20] Test flag values for LD and OF threaded/asyncio, not just flag names --- .../launchdarkly/test_launchdarkly.py | 24 ++++++++++++++----- .../openfeature/test_openfeature.py | 24 ++++++++++++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index acbe764104..7be46f91af 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -57,7 +57,7 @@ def task(flag_key): # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): client.variation(flag_key, context, False) - return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + return sentry_sdk.get_current_scope().flags.get() td.update(td.flag("hello").variation_for_all(True)) td.update(td.flag("world").variation_for_all(False)) @@ -67,8 +67,14 @@ def task(flag_key): with cf.ThreadPoolExecutor(max_workers=2) as pool: results = list(pool.map(task, ["world", "other"])) - assert results[0] == ["hello", "world"] - assert results[1] == ["hello", "other"] + assert results[0] == [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] def test_launchdarkly_integration_asyncio(sentry_init): @@ -81,7 +87,7 @@ def test_launchdarkly_integration_asyncio(sentry_init): async def task(flag_key): with sentry_sdk.isolation_scope(): client.variation(flag_key, context, False) - return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + return sentry_sdk.get_current_scope().flags.get() async def runner(): return asyncio.gather(task("world"), task("other")) @@ -91,8 +97,14 @@ async def runner(): client.variation("hello", context, False) results = asyncio.run(runner()).result() - assert results[0] == ["hello", "world"] - assert results[1] == ["hello", "other"] + assert results[0] == [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] def test_launchdarkly_integration_did_not_enable(monkeypatch): diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 24e7857f9a..7219f155a6 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -44,13 +44,19 @@ def task(flag): # Create a new isolation scope for the thread. This means the flags with sentry_sdk.isolation_scope(): client.get_boolean_value(flag, default_value=False) - return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + return sentry_sdk.get_current_scope().flags.get() with cf.ThreadPoolExecutor(max_workers=2) as pool: results = list(pool.map(task, ["world", "other"])) - assert results[0] == ["hello", "world"] - assert results[1] == ["hello", "other"] + assert results[0] == [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] def test_openfeature_integration_asyncio(sentry_init): @@ -59,7 +65,7 @@ def test_openfeature_integration_asyncio(sentry_init): async def task(flag): with sentry_sdk.isolation_scope(): client.get_boolean_value(flag, default_value=False) - return [f["flag"] for f in sentry_sdk.get_current_scope().flags.get()] + return sentry_sdk.get_current_scope().flags.get() async def runner(): return asyncio.gather(task("world"), task("other")) @@ -76,5 +82,11 @@ async def runner(): client.get_boolean_value("hello", default_value=False) results = asyncio.run(runner()).result() - assert results[0] == ["hello", "world"] - assert results[1] == ["hello", "other"] + assert results[0] == [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + assert results[1] == [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] From beb951210279b4a5f05a7961ab83b9f08e1a6895 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 5 Dec 2024 11:56:57 -0800 Subject: [PATCH 03/20] update ffIntegration test to be e2e, and fix LRU copy bug --- sentry_sdk/_lru_cache.py | 7 +- .../featureflags/test_featureflags.py | 118 ++++++++++++------ tests/test_lru_cache.py | 19 +++ 3 files changed, 105 insertions(+), 39 deletions(-) diff --git a/sentry_sdk/_lru_cache.py b/sentry_sdk/_lru_cache.py index ec557b1093..fff2fb85e5 100644 --- a/sentry_sdk/_lru_cache.py +++ b/sentry_sdk/_lru_cache.py @@ -62,7 +62,7 @@ """ -from copy import copy +from copy import copy, deepcopy SENTINEL = object() @@ -92,10 +92,13 @@ def __init__(self, max_size): self.hits = self.misses = 0 def __copy__(self): + """ + Cache keys and values are shallow copied. + """ cache = LRUCache(self.max_size) cache.full = self.full cache.cache = copy(self.cache) - cache.root = copy(self.root) + cache.root = deepcopy(self.root) return cache def set(self, key, value): diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index f89d64e9b3..4d2eb1731a 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -2,10 +2,14 @@ import concurrent.futures as cf import sentry_sdk +from sentry_sdk.integrations import _processed_integrations from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration -def test_featureflags_integration(sentry_init): +def test_featureflags_integration(sentry_init, capture_events): + _processed_integrations.discard( + FeatureFlagsIntegration.identifier + ) # force reinstall sentry_init(integrations=[FeatureFlagsIntegration()]) flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) @@ -13,61 +17,101 @@ def test_featureflags_integration(sentry_init): flags_integration.set_flag("world", True) flags_integration.set_flag("other", False) - assert sentry_sdk.get_current_scope().flags.get() == [ - {"flag": "hello", "result": False}, - {"flag": "world", "result": True}, - {"flag": "other", "result": False}, - ] + events = capture_events() + sentry_sdk.capture_exception(Exception("something wrong!")) + [event] = events + assert event["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": True}, + {"flag": "other", "result": False}, + ] + } -def test_featureflags_integration_threaded(sentry_init): + +def test_featureflags_integration_threaded(sentry_init, capture_events): + _processed_integrations.discard( + FeatureFlagsIntegration.identifier + ) # force reinstall sentry_init(integrations=[FeatureFlagsIntegration()]) + events = capture_events() + + # Capture an eval before we split isolation scopes. flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) + flags_integration.set_flag("hello", False) def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): + flags_integration = sentry_sdk.get_client().get_integration( + FeatureFlagsIntegration + ) flags_integration.set_flag(flag_key, False) - return sentry_sdk.get_current_scope().flags.get() - - # Capture an eval before we split isolation scopes. - flags_integration.set_flag("hello", False) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("flag_key", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: - results = list(pool.map(task, ["world", "other"])) - - assert results[0] == [ - {"flag": "hello", "result": False}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": False}, - {"flag": "other", "result": False}, - ] - - -def test_featureflags_integration_asyncio(sentry_init): - """Assert concurrently evaluated flags do not pollute one another.""" + pool.map(task, ["world", "other"]) + + assert len(events) == 2 + events.sort(key=lambda e: e["tags"]["flag_key"]) + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": False}, + ] + } + + +def test_featureflags_integration_asyncio(sentry_init, capture_events): + _processed_integrations.discard( + FeatureFlagsIntegration.identifier + ) # force reinstall sentry_init(integrations=[FeatureFlagsIntegration()]) + events = capture_events() + + # Capture an eval before we split isolation scopes. flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) + flags_integration.set_flag("hello", False) async def task(flag_key): + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): + flags_integration = sentry_sdk.get_client().get_integration( + FeatureFlagsIntegration + ) flags_integration.set_flag(flag_key, False) - return sentry_sdk.get_current_scope().flags.get() + # use a tag to identify to identify events later on + sentry_sdk.set_tag("flag_key", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) async def runner(): return asyncio.gather(task("world"), task("other")) - flags_integration.set_flag("hello", False) - - results = asyncio.run(runner()).result() - assert results[0] == [ - {"flag": "hello", "result": False}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": False}, - {"flag": "other", "result": False}, - ] + asyncio.run(runner()) + + assert len(events) == 2 + events.sort(key=lambda e: e["tags"]["flag_key"]) + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": False}, + ] + } diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index 3e9c0ac964..92c8257eaf 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -1,3 +1,5 @@ +from copy import copy + import pytest from sentry_sdk._lru_cache import LRUCache @@ -58,3 +60,20 @@ def test_cache_get_all(): assert cache.get_all() == [(1, 1), (2, 2), (3, 3)] cache.get(1) assert cache.get_all() == [(2, 2), (3, 3), (1, 1)] + + +def test_cache_copy(): + cache = LRUCache(3) + cache.set(0, 0) + cache.set(1, 1) + + copied = copy(cache) + cache.set(2, 2) + cache.set(3, 3) + assert copied.get_all() == [(0, 0), (1, 1)] + assert cache.get_all() == [(1, 1), (2, 2), (3, 3)] + + copied = copy(cache) + cache.get(1) + assert copied.get_all() == [(1, 1), (2, 2), (3, 3)] + assert cache.get_all() == [(2, 2), (3, 3), (1, 1)] From 921b133b8d9c244d140cee3f113b185693eba675 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:02:36 -0800 Subject: [PATCH 04/20] make a helper fixture and test error processor in original thread --- .../featureflags/test_featureflags.py | 73 +++++++++++++------ 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 4d2eb1731a..202107d603 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,15 +1,26 @@ import asyncio import concurrent.futures as cf +import pytest + import sentry_sdk -from sentry_sdk.integrations import _processed_integrations +from sentry_sdk.integrations import _processed_integrations, _installed_integrations from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration -def test_featureflags_integration(sentry_init, capture_events): - _processed_integrations.discard( - FeatureFlagsIntegration.identifier - ) # force reinstall +@pytest.fixture +def uninstall_integration(): + """Forces the next call to sentry_init to re-install/setup an integration.""" + + def inner(identifier): + _processed_integrations.discard(identifier) + _installed_integrations.discard(identifier) + + return inner + + +def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) @@ -30,10 +41,10 @@ def test_featureflags_integration(sentry_init, capture_events): } -def test_featureflags_integration_threaded(sentry_init, capture_events): - _processed_integrations.discard( - FeatureFlagsIntegration.identifier - ) # force reinstall +def test_featureflags_integration_threaded( + sentry_init, capture_events, uninstall_integration +): + uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) events = capture_events() @@ -50,22 +61,32 @@ def task(flag_key): ) flags_integration.set_flag(flag_key, False) # use a tag to identify to identify events later on - sentry_sdk.set_tag("flag_key", flag_key) + sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: pool.map(task, ["world", "other"]) - assert len(events) == 2 - events.sort(key=lambda e: e["tags"]["flag_key"]) + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + assert events[0]["contexts"]["flags"] == { "values": [ {"flag": "hello", "result": False}, - {"flag": "other", "result": False}, ] } assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { "values": [ {"flag": "hello", "result": False}, {"flag": "world", "result": False}, @@ -73,10 +94,10 @@ def task(flag_key): } -def test_featureflags_integration_asyncio(sentry_init, capture_events): - _processed_integrations.discard( - FeatureFlagsIntegration.identifier - ) # force reinstall +def test_featureflags_integration_asyncio( + sentry_init, capture_events, uninstall_integration +): + uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) events = capture_events() @@ -93,7 +114,7 @@ async def task(flag_key): ) flags_integration.set_flag(flag_key, False) # use a tag to identify to identify events later on - sentry_sdk.set_tag("flag_key", flag_key) + sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) async def runner(): @@ -101,15 +122,25 @@ async def runner(): asyncio.run(runner()) - assert len(events) == 2 - events.sort(key=lambda e: e["tags"]["flag_key"]) + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + assert events[0]["contexts"]["flags"] == { "values": [ {"flag": "hello", "result": False}, - {"flag": "other", "result": False}, ] } assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { "values": [ {"flag": "hello", "result": False}, {"flag": "world", "result": False}, From 4651b6ac135a270d5e9f8eaca01899d5f7608da0 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:04:34 -0800 Subject: [PATCH 05/20] Move api to top-level, rename to add_flag --- sentry_sdk/integrations/featureflags.py | 8 +++--- .../featureflags/test_featureflags.py | 25 ++++++------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index 8a1e6b4106..6e995ac28b 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -12,7 +12,7 @@ def setup_once(): scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) - @staticmethod - def set_flag(flag: str, result: bool): - flags = sentry_sdk.get_current_scope().flags - flags.set(flag, result) + +def add_flag(flag: str, result: bool): + flags = sentry_sdk.get_current_scope().flags + flags.set(flag, result) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 202107d603..4677870d50 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -5,7 +5,7 @@ import sentry_sdk from sentry_sdk.integrations import _processed_integrations, _installed_integrations -from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration +from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_flag @pytest.fixture @@ -22,11 +22,10 @@ def inner(identifier): def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) - flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) - flags_integration.set_flag("hello", False) - flags_integration.set_flag("world", True) - flags_integration.set_flag("other", False) + add_flag("hello", False) + add_flag("world", True) + add_flag("other", False) events = capture_events() sentry_sdk.capture_exception(Exception("something wrong!")) @@ -49,17 +48,13 @@ def test_featureflags_integration_threaded( events = capture_events() # Capture an eval before we split isolation scopes. - flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) - flags_integration.set_flag("hello", False) + add_flag("hello", False) def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): - flags_integration = sentry_sdk.get_client().get_integration( - FeatureFlagsIntegration - ) - flags_integration.set_flag(flag_key, False) + add_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) @@ -102,17 +97,13 @@ def test_featureflags_integration_asyncio( events = capture_events() # Capture an eval before we split isolation scopes. - flags_integration = sentry_sdk.get_client().get_integration(FeatureFlagsIntegration) - flags_integration.set_flag("hello", False) + add_flag("hello", False) async def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): - flags_integration = sentry_sdk.get_client().get_integration( - FeatureFlagsIntegration - ) - flags_integration.set_flag(flag_key, False) + add_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) From 36bc8698dfbae98effae627cb9312eca228a64c6 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:19:45 -0800 Subject: [PATCH 06/20] Add docstrs --- sentry_sdk/integrations/featureflags.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index 6e995ac28b..669341bb6e 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -5,6 +5,26 @@ class FeatureFlagsIntegration(Integration): + """ + Sentry integration for buffering feature flags manually with an API and capturing them on + error events. We recommend you do this on each flag evaluation. Flags are buffered per Sentry + scope and limited to 100 per event. + + See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) + for more information. + + @example + ``` + import sentry_sdk + from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_flag + + sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]); + + add_flag('my-flag', true); + sentry_sdk.capture_exception(Exception('broke')); // 'my-flag' should be captured on this Sentry event. + ``` + """ + identifier = "featureflags" @staticmethod @@ -14,5 +34,9 @@ def setup_once(): def add_flag(flag: str, result: bool): + """ + 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 and limited to 100 per event. + """ flags = sentry_sdk.get_current_scope().flags flags.set(flag, result) From 81c17617efb26419673cbae4c1a9de8bbfbd5f9c Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:20:40 -0800 Subject: [PATCH 07/20] Rename to add_feature_flag --- sentry_sdk/integrations/featureflags.py | 6 +++--- .../featureflags/test_featureflags.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index 669341bb6e..b13dfc03e8 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -16,11 +16,11 @@ class FeatureFlagsIntegration(Integration): @example ``` import sentry_sdk - from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_flag + from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_feature_flag sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]); - add_flag('my-flag', true); + add_feature_flag('my-flag', true); sentry_sdk.capture_exception(Exception('broke')); // 'my-flag' should be captured on this Sentry event. ``` """ @@ -33,7 +33,7 @@ def setup_once(): scope.add_error_processor(flag_error_processor) -def add_flag(flag: str, result: bool): +def add_feature_flag(flag: str, result: bool): """ 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 and limited to 100 per event. diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 4677870d50..29d7026a66 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -5,7 +5,10 @@ import sentry_sdk from sentry_sdk.integrations import _processed_integrations, _installed_integrations -from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_flag +from sentry_sdk.integrations.featureflags import ( + FeatureFlagsIntegration, + add_feature_flag, +) @pytest.fixture @@ -23,9 +26,9 @@ def test_featureflags_integration(sentry_init, capture_events, uninstall_integra uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) - add_flag("hello", False) - add_flag("world", True) - add_flag("other", False) + add_feature_flag("hello", False) + add_feature_flag("world", True) + add_feature_flag("other", False) events = capture_events() sentry_sdk.capture_exception(Exception("something wrong!")) @@ -48,13 +51,13 @@ def test_featureflags_integration_threaded( events = capture_events() # Capture an eval before we split isolation scopes. - add_flag("hello", False) + add_feature_flag("hello", False) def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): - add_flag(flag_key, False) + add_feature_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) @@ -97,13 +100,13 @@ def test_featureflags_integration_asyncio( events = capture_events() # Capture an eval before we split isolation scopes. - add_flag("hello", False) + add_feature_flag("hello", False) async def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): - add_flag(flag_key, False) + add_feature_flag(flag_key, False) # use a tag to identify to identify events later on sentry_sdk.set_tag("task_id", flag_key) sentry_sdk.capture_exception(Exception("something wrong!")) From 42f76a36a7784efa6f8c0ce68838d28098e63234 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:28:12 -0800 Subject: [PATCH 08/20] Rm extra import in test_lru_cache --- tests/test_lru_cache.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_lru_cache.py b/tests/test_lru_cache.py index a6e040c111..cab9bbc7eb 100644 --- a/tests/test_lru_cache.py +++ b/tests/test_lru_cache.py @@ -1,5 +1,3 @@ -from copy import copy - import pytest from copy import copy From ef01990d64704576742b41b3b3861e87eaaf265f Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:29:27 -0800 Subject: [PATCH 09/20] Revert lru comment --- sentry_sdk/_lru_cache.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry_sdk/_lru_cache.py b/sentry_sdk/_lru_cache.py index 15520a49a6..825c773529 100644 --- a/sentry_sdk/_lru_cache.py +++ b/sentry_sdk/_lru_cache.py @@ -92,9 +92,6 @@ def __init__(self, max_size): self.hits = self.misses = 0 def __copy__(self): - """ - Cache keys and values are shallow copied. - """ cache = LRUCache(self.max_size) cache.full = self.full cache.cache = copy(self.cache) From 381ccc1e5e30ccbddc5c58cb94afad8e8f770d1d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 11:51:20 -0800 Subject: [PATCH 10/20] Type annotate --- sentry_sdk/integrations/featureflags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index b13dfc03e8..6e840e6906 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -29,11 +29,13 @@ class FeatureFlagsIntegration(Integration): @staticmethod def setup_once(): + # type: () -> None scope = sentry_sdk.get_current_scope() scope.add_error_processor(flag_error_processor) -def add_feature_flag(flag: str, result: bool): +def add_feature_flag(flag, result): + # type: (str, bool) -> None """ 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 and limited to 100 per event. From c76192e25a495168818eb4b90223a244bc4149af Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:17:57 -0800 Subject: [PATCH 11/20] Review comments --- sentry_sdk/integrations/featureflags.py | 2 +- tests/integrations/featureflags/test_featureflags.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index 6e840e6906..17bb02a392 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -8,7 +8,7 @@ class FeatureFlagsIntegration(Integration): """ Sentry integration for buffering feature flags manually with an API and capturing them on error events. We recommend you do this on each flag evaluation. Flags are buffered per Sentry - scope and limited to 100 per event. + scope. See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 29d7026a66..801318ff91 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -32,9 +32,9 @@ def test_featureflags_integration(sentry_init, capture_events, uninstall_integra events = capture_events() sentry_sdk.capture_exception(Exception("something wrong!")) - [event] = events - assert event["contexts"]["flags"] == { + assert len(events) == 1 + assert events[0]["contexts"]["flags"] == { "values": [ {"flag": "hello", "result": False}, {"flag": "world", "result": True}, From b63982b42e253911b11604e15558973289abdeb1 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:42:52 -0800 Subject: [PATCH 12/20] Update launchdarkly and openfeature tests to be e2e --- tests/conftest.py | 11 ++ .../featureflags/test_featureflags.py | 14 -- .../launchdarkly/test_launchdarkly.py | 124 +++++++++++++----- .../openfeature/test_openfeature.py | 115 +++++++++++----- 4 files changed, 187 insertions(+), 77 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 64527c1e36..c0383d94b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -184,6 +184,17 @@ def reset_integrations(): _installed_integrations.clear() +@pytest.fixture +def uninstall_integration(): + """Use to force the next call to sentry_init to re-install/setup an integration.""" + + def inner(identifier): + _processed_integrations.discard(identifier) + _installed_integrations.discard(identifier) + + return inner + + @pytest.fixture def sentry_init(request): def inner(*a, **kw): diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 801318ff91..1041058e4d 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,27 +1,13 @@ import asyncio import concurrent.futures as cf -import pytest - import sentry_sdk -from sentry_sdk.integrations import _processed_integrations, _installed_integrations from sentry_sdk.integrations.featureflags import ( FeatureFlagsIntegration, add_feature_flag, ) -@pytest.fixture -def uninstall_integration(): - """Forces the next call to sentry_init to re-install/setup an integration.""" - - def inner(identifier): - _processed_integrations.discard(identifier) - _installed_integrations.discard(identifier) - - return inner - - def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 7be46f91af..17e108508b 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -19,9 +19,13 @@ "use_global_client", (False, True), ) -def test_launchdarkly_integration(sentry_init, use_global_client): +def test_launchdarkly_integration( + sentry_init, use_global_client, capture_events, uninstall_integration +): td = TestData.data_source() config = Config("sdk-key", update_processor_class=td) + + uninstall_integration(LaunchDarklyIntegration.identifier) if use_global_client: ldclient.set_config(config) sentry_init(integrations=[LaunchDarklyIntegration()]) @@ -39,25 +43,38 @@ def test_launchdarkly_integration(sentry_init, use_global_client): client.variation("world", Context.create("user1", "user"), False) client.variation("other", Context.create("user2", "user"), False) - assert sentry_sdk.get_current_scope().flags.get() == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": True}, - {"flag": "other", "result": False}, - ] + events = capture_events() + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 1 + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": True}, + {"flag": "other", "result": False}, + ] + } -def test_launchdarkly_integration_threaded(sentry_init): +def test_launchdarkly_integration_threaded( + sentry_init, capture_events, uninstall_integration +): td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") + uninstall_integration(LaunchDarklyIntegration.identifier) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) + events = capture_events() + def task(flag_key): # Creates a new isolation scope for the thread. # This means the evaluations in each task are captured separately. with sentry_sdk.isolation_scope(): client.variation(flag_key, context, False) - return sentry_sdk.get_current_scope().flags.get() + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) td.update(td.flag("hello").variation_for_all(True)) td.update(td.flag("world").variation_for_all(False)) @@ -65,46 +82,87 @@ def task(flag_key): client.variation("hello", context, False) with cf.ThreadPoolExecutor(max_workers=2) as pool: - results = list(pool.map(task, ["world", "other"])) - - assert results[0] == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": True}, - {"flag": "other", "result": False}, - ] - - -def test_launchdarkly_integration_asyncio(sentry_init): + pool.map(task, ["world", "other"]) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } + + +def test_launchdarkly_integration_asyncio( + sentry_init, capture_events, uninstall_integration +): """Assert concurrently evaluated flags do not pollute one another.""" td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) - sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) context = Context.create("user1") + uninstall_integration(LaunchDarklyIntegration.identifier) + sentry_init(integrations=[LaunchDarklyIntegration(ld_client=client)]) + events = capture_events() + async def task(flag_key): with sentry_sdk.isolation_scope(): client.variation(flag_key, context, False) - return sentry_sdk.get_current_scope().flags.get() + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) async def runner(): return asyncio.gather(task("world"), task("other")) td.update(td.flag("hello").variation_for_all(True)) td.update(td.flag("world").variation_for_all(False)) + # Capture an eval before we split isolation scopes. client.variation("hello", context, False) - results = asyncio.run(runner()).result() - assert results[0] == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": True}, - {"flag": "other", "result": False}, - ] + asyncio.run(runner()) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } def test_launchdarkly_integration_did_not_enable(monkeypatch): diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 7219f155a6..4061314e81 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -7,7 +7,8 @@ from sentry_sdk.integrations.openfeature import OpenFeatureIntegration -def test_openfeature_integration(sentry_init): +def test_openfeature_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(OpenFeatureIntegration.identifier) sentry_init(integrations=[OpenFeatureIntegration()]) flags = { @@ -21,15 +22,25 @@ def test_openfeature_integration(sentry_init): client.get_boolean_value("world", default_value=False) client.get_boolean_value("other", default_value=True) - assert sentry_sdk.get_current_scope().flags.get() == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": False}, - {"flag": "other", "result": True}, - ] + events = capture_events() + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 1 + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + {"flag": "other", "result": True}, + ] + } -def test_openfeature_integration_threaded(sentry_init): +def test_openfeature_integration_threaded( + sentry_init, capture_events, uninstall_integration +): + uninstall_integration(OpenFeatureIntegration.identifier) sentry_init(integrations=[OpenFeatureIntegration()]) + events = capture_events() flags = { "hello": InMemoryFlag("on", {"on": True, "off": False}), @@ -37,6 +48,7 @@ def test_openfeature_integration_threaded(sentry_init): } api.set_provider(InMemoryProvider(flags)) + # Capture an eval before we split isolation scopes. client = api.get_client() client.get_boolean_value("hello", default_value=False) @@ -44,49 +56,92 @@ def task(flag): # Create a new isolation scope for the thread. This means the flags with sentry_sdk.isolation_scope(): client.get_boolean_value(flag, default_value=False) - return sentry_sdk.get_current_scope().flags.get() + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag) + sentry_sdk.capture_exception(Exception("something wrong!")) + # Run tasks in separate threads with cf.ThreadPoolExecutor(max_workers=2) as pool: - results = list(pool.map(task, ["world", "other"])) + pool.map(task, ["world", "other"]) - assert results[0] == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": True}, - {"flag": "other", "result": False}, - ] + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) -def test_openfeature_integration_asyncio(sentry_init): + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } + + +def test_openfeature_integration_asyncio( + sentry_init, capture_events, uninstall_integration +): """Assert concurrently evaluated flags do not pollute one another.""" + uninstall_integration(OpenFeatureIntegration.identifier) + sentry_init(integrations=[OpenFeatureIntegration()]) + events = capture_events() + async def task(flag): with sentry_sdk.isolation_scope(): client.get_boolean_value(flag, default_value=False) - return sentry_sdk.get_current_scope().flags.get() + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag) + sentry_sdk.capture_exception(Exception("something wrong!")) async def runner(): return asyncio.gather(task("world"), task("other")) - sentry_init(integrations=[OpenFeatureIntegration()]) - flags = { "hello": InMemoryFlag("on", {"on": True, "off": False}), "world": InMemoryFlag("off", {"on": True, "off": False}), } api.set_provider(InMemoryProvider(flags)) + # Capture an eval before we split isolation scopes. client = api.get_client() client.get_boolean_value("hello", default_value=False) - results = asyncio.run(runner()).result() - assert results[0] == [ - {"flag": "hello", "result": True}, - {"flag": "world", "result": False}, - ] - assert results[1] == [ - {"flag": "hello", "result": True}, - {"flag": "other", "result": False}, - ] + asyncio.run(runner()) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": True}, + {"flag": "world", "result": False}, + ] + } From b4eb42184a9ae00c56caf7613a9a39f4c8e8428c Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:08:04 -0800 Subject: [PATCH 13/20] Update docstrs --- sentry_sdk/integrations/featureflags.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sentry_sdk/integrations/featureflags.py b/sentry_sdk/integrations/featureflags.py index 17bb02a392..46947eec72 100644 --- a/sentry_sdk/integrations/featureflags.py +++ b/sentry_sdk/integrations/featureflags.py @@ -6,9 +6,9 @@ class FeatureFlagsIntegration(Integration): """ - Sentry integration for buffering feature flags manually with an API and capturing them on - error events. We recommend you do this on each flag evaluation. Flags are buffered per Sentry - scope. + Sentry integration for capturing feature flags on error events. To manually buffer flag data, + call `integrations.featureflags.add_feature_flag`. We recommend you do this on each flag + evaluation. See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. @@ -37,8 +37,8 @@ def setup_once(): def add_feature_flag(flag, result): # type: (str, bool) -> None """ - 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 and limited to 100 per event. + Records a flag and its value to be sent on subsequent error events by FeatureFlagsIntegration. + We recommend you do this on flag evaluations. Flags are buffered per Sentry scope. """ flags = sentry_sdk.get_current_scope().flags flags.set(flag, result) From 043e298717335bf86c46c5b1d24fa9f90ea0e6f4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:58:20 -0800 Subject: [PATCH 14/20] Skip threading test for <3.7 --- tests/integrations/threading/test_threading.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index 0d14fae352..a0e0f2682d 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -1,4 +1,5 @@ import gc +import sys from concurrent import futures from threading import Thread @@ -124,6 +125,9 @@ def run(self): assert unreachable_objects == 0 +@pytest.mark.skipif( + sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" +) def test_double_patching(sentry_init, capture_events): sentry_init(default_integrations=False, integrations=[ThreadingIntegration()]) events = capture_events() From 2dc679d5a22a64b2062a3d1fd1e915e540b35e1e Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:11:45 -0800 Subject: [PATCH 15/20] Skip ffs asyncio test if 3.6 --- tests/integrations/featureflags/test_featureflags.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 1041058e4d..f1ac83eecd 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -7,6 +7,9 @@ add_feature_flag, ) +import pytest +import sys + def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): uninstall_integration(FeatureFlagsIntegration.identifier) @@ -78,6 +81,9 @@ def task(flag_key): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" +) def test_featureflags_integration_asyncio( sentry_init, capture_events, uninstall_integration ): From af9e92da338a538342571b7a9b6d7612d636546d Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:43:37 -0800 Subject: [PATCH 16/20] undo 'skip threading test' --- tests/integrations/threading/test_threading.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integrations/threading/test_threading.py b/tests/integrations/threading/test_threading.py index a0e0f2682d..0d14fae352 100644 --- a/tests/integrations/threading/test_threading.py +++ b/tests/integrations/threading/test_threading.py @@ -1,5 +1,4 @@ import gc -import sys from concurrent import futures from threading import Thread @@ -125,9 +124,6 @@ def run(self): assert unreachable_objects == 0 -@pytest.mark.skipif( - sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" -) def test_double_patching(sentry_init, capture_events): sentry_init(default_integrations=False, integrations=[ThreadingIntegration()]) events = capture_events() From 2cea37bb310970d2c7c0b92fc82f80260fe725b4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 17:49:11 -0800 Subject: [PATCH 17/20] Try commenting out asyncio --- .../featureflags/test_featureflags.py | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index f1ac83eecd..f6d85d5269 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,4 +1,4 @@ -import asyncio +# import asyncio import concurrent.futures as cf import sentry_sdk @@ -7,8 +7,8 @@ add_feature_flag, ) -import pytest -import sys +# import pytest +# import sys def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): @@ -81,54 +81,56 @@ def task(flag_key): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" -) -def test_featureflags_integration_asyncio( - sentry_init, capture_events, uninstall_integration -): - uninstall_integration(FeatureFlagsIntegration.identifier) - sentry_init(integrations=[FeatureFlagsIntegration()]) - events = capture_events() - - # Capture an eval before we split isolation scopes. - add_feature_flag("hello", False) - - async def task(flag_key): - # Creates a new isolation scope for the thread. - # This means the evaluations in each task are captured separately. - with sentry_sdk.isolation_scope(): - add_feature_flag(flag_key, False) - # use a tag to identify to identify events later on - sentry_sdk.set_tag("task_id", flag_key) - sentry_sdk.capture_exception(Exception("something wrong!")) - - async def runner(): - return asyncio.gather(task("world"), task("other")) - - asyncio.run(runner()) - - # Capture error in original scope - sentry_sdk.set_tag("task_id", "0") - sentry_sdk.capture_exception(Exception("something wrong!")) - - assert len(events) == 3 - events.sort(key=lambda e: e["tags"]["task_id"]) - - assert events[0]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - ] - } - assert events[1]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - {"flag": "other", "result": False}, - ] - } - assert events[2]["contexts"]["flags"] == { - "values": [ - {"flag": "hello", "result": False}, - {"flag": "world", "result": False}, - ] - } +# +# +# @pytest.mark.skipif( +# sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" +# ) +# def test_featureflags_integration_asyncio( +# sentry_init, capture_events, uninstall_integration +# ): +# uninstall_integration(FeatureFlagsIntegration.identifier) +# sentry_init(integrations=[FeatureFlagsIntegration()]) +# events = capture_events() +# +# # Capture an eval before we split isolation scopes. +# add_feature_flag("hello", False) +# +# async def task(flag_key): +# # Creates a new isolation scope for the thread. +# # This means the evaluations in each task are captured separately. +# with sentry_sdk.isolation_scope(): +# add_feature_flag(flag_key, False) +# # use a tag to identify to identify events later on +# sentry_sdk.set_tag("task_id", flag_key) +# sentry_sdk.capture_exception(Exception("something wrong!")) +# +# async def runner(): +# return asyncio.gather(task("world"), task("other")) +# +# asyncio.run(runner()) +# +# # Capture error in original scope +# sentry_sdk.set_tag("task_id", "0") +# sentry_sdk.capture_exception(Exception("something wrong!")) +# +# assert len(events) == 3 +# events.sort(key=lambda e: e["tags"]["task_id"]) +# +# assert events[0]["contexts"]["flags"] == { +# "values": [ +# {"flag": "hello", "result": False}, +# ] +# } +# assert events[1]["contexts"]["flags"] == { +# "values": [ +# {"flag": "hello", "result": False}, +# {"flag": "other", "result": False}, +# ] +# } +# assert events[2]["contexts"]["flags"] == { +# "values": [ +# {"flag": "hello", "result": False}, +# {"flag": "world", "result": False}, +# ] +# } From fea761ce9ed88f58a4e8f8cfe72f54d34de70850 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 19:59:01 -0800 Subject: [PATCH 18/20] Use importorskip --- .../featureflags/test_featureflags.py | 107 +++++++++--------- .../launchdarkly/test_launchdarkly.py | 4 +- .../openfeature/test_openfeature.py | 4 +- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index f6d85d5269..7631ccfd2e 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,4 +1,3 @@ -# import asyncio import concurrent.futures as cf import sentry_sdk @@ -7,8 +6,7 @@ add_feature_flag, ) -# import pytest -# import sys +import pytest def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): @@ -81,56 +79,53 @@ def task(flag_key): } -# -# -# @pytest.mark.skipif( -# sys.version_info < (3, 7), reason="Test requires Python 3.7 or higher" -# ) -# def test_featureflags_integration_asyncio( -# sentry_init, capture_events, uninstall_integration -# ): -# uninstall_integration(FeatureFlagsIntegration.identifier) -# sentry_init(integrations=[FeatureFlagsIntegration()]) -# events = capture_events() -# -# # Capture an eval before we split isolation scopes. -# add_feature_flag("hello", False) -# -# async def task(flag_key): -# # Creates a new isolation scope for the thread. -# # This means the evaluations in each task are captured separately. -# with sentry_sdk.isolation_scope(): -# add_feature_flag(flag_key, False) -# # use a tag to identify to identify events later on -# sentry_sdk.set_tag("task_id", flag_key) -# sentry_sdk.capture_exception(Exception("something wrong!")) -# -# async def runner(): -# return asyncio.gather(task("world"), task("other")) -# -# asyncio.run(runner()) -# -# # Capture error in original scope -# sentry_sdk.set_tag("task_id", "0") -# sentry_sdk.capture_exception(Exception("something wrong!")) -# -# assert len(events) == 3 -# events.sort(key=lambda e: e["tags"]["task_id"]) -# -# assert events[0]["contexts"]["flags"] == { -# "values": [ -# {"flag": "hello", "result": False}, -# ] -# } -# assert events[1]["contexts"]["flags"] == { -# "values": [ -# {"flag": "hello", "result": False}, -# {"flag": "other", "result": False}, -# ] -# } -# assert events[2]["contexts"]["flags"] == { -# "values": [ -# {"flag": "hello", "result": False}, -# {"flag": "world", "result": False}, -# ] -# } +def test_featureflags_integration_asyncio( + sentry_init, capture_events, uninstall_integration +): + asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + + uninstall_integration(FeatureFlagsIntegration.identifier) + sentry_init(integrations=[FeatureFlagsIntegration()]) + events = capture_events() + + # Capture an eval before we split isolation scopes. + add_feature_flag("hello", False) + + async def task(flag_key): + # Creates a new isolation scope for the thread. + # This means the evaluations in each task are captured separately. + with sentry_sdk.isolation_scope(): + add_feature_flag(flag_key, False) + # use a tag to identify to identify events later on + sentry_sdk.set_tag("task_id", flag_key) + sentry_sdk.capture_exception(Exception("something wrong!")) + + async def runner(): + return asyncio.gather(task("world"), task("other")) + + asyncio.run(runner()) + + # Capture error in original scope + sentry_sdk.set_tag("task_id", "0") + sentry_sdk.capture_exception(Exception("something wrong!")) + + assert len(events) == 3 + events.sort(key=lambda e: e["tags"]["task_id"]) + + assert events[0]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + ] + } + assert events[1]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "other", "result": False}, + ] + } + assert events[2]["contexts"]["flags"] == { + "values": [ + {"flag": "hello", "result": False}, + {"flag": "world", "result": False}, + ] + } diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 17e108508b..5fd61deeca 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -1,4 +1,3 @@ -import asyncio import concurrent.futures as cf import ldclient @@ -114,6 +113,9 @@ def test_launchdarkly_integration_asyncio( sentry_init, capture_events, uninstall_integration ): """Assert concurrently evaluated flags do not pollute one another.""" + + asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) context = Context.create("user1") diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 4061314e81..48d53dd9c3 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -1,10 +1,10 @@ -import asyncio import concurrent.futures as cf import sentry_sdk from openfeature import api from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider from sentry_sdk.integrations.openfeature import OpenFeatureIntegration +import pytest def test_openfeature_integration(sentry_init, capture_events, uninstall_integration): @@ -95,6 +95,8 @@ def test_openfeature_integration_asyncio( ): """Assert concurrently evaluated flags do not pollute one another.""" + asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + uninstall_integration(OpenFeatureIntegration.identifier) sentry_init(integrations=[OpenFeatureIntegration()]) events = capture_events() From 752ce7d76f7bad924772a58bb0e13ad65220f28a Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:00:15 -0800 Subject: [PATCH 19/20] Import order --- tests/integrations/featureflags/test_featureflags.py | 3 +-- tests/integrations/launchdarkly/test_launchdarkly.py | 4 +--- tests/integrations/openfeature/test_openfeature.py | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index 7631ccfd2e..ea3cb5e9bb 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,4 +1,5 @@ import concurrent.futures as cf +import pytest import sentry_sdk from sentry_sdk.integrations.featureflags import ( @@ -6,8 +7,6 @@ add_feature_flag, ) -import pytest - def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): uninstall_integration(FeatureFlagsIntegration.identifier) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 5fd61deeca..af3d46ba4f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -1,8 +1,5 @@ import concurrent.futures as cf - import ldclient - -import sentry_sdk import pytest from ldclient import LDClient @@ -10,6 +7,7 @@ from ldclient.context import Context from ldclient.integrations.test_data import TestData +import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 48d53dd9c3..4c581dff0b 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -1,10 +1,11 @@ import concurrent.futures as cf -import sentry_sdk +import pytest from openfeature import api from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider + +import sentry_sdk from sentry_sdk.integrations.openfeature import OpenFeatureIntegration -import pytest def test_openfeature_integration(sentry_init, capture_events, uninstall_integration): From d77ebc3ad245223733478c01a9d8edec00421f22 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 18 Dec 2024 20:22:44 -0800 Subject: [PATCH 20/20] Also use skipif --- tests/integrations/featureflags/test_featureflags.py | 5 ++++- tests/integrations/launchdarkly/test_launchdarkly.py | 5 ++++- tests/integrations/openfeature/test_openfeature.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/integrations/featureflags/test_featureflags.py b/tests/integrations/featureflags/test_featureflags.py index ea3cb5e9bb..539e910607 100644 --- a/tests/integrations/featureflags/test_featureflags.py +++ b/tests/integrations/featureflags/test_featureflags.py @@ -1,4 +1,6 @@ import concurrent.futures as cf +import sys + import pytest import sentry_sdk @@ -78,10 +80,11 @@ def task(flag_key): } +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_featureflags_integration_asyncio( sentry_init, capture_events, uninstall_integration ): - asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + asyncio = pytest.importorskip("asyncio") uninstall_integration(FeatureFlagsIntegration.identifier) sentry_init(integrations=[FeatureFlagsIntegration()]) diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index af3d46ba4f..f66a4219ec 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -1,4 +1,6 @@ import concurrent.futures as cf +import sys + import ldclient import pytest @@ -107,12 +109,13 @@ def task(flag_key): } +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_launchdarkly_integration_asyncio( sentry_init, capture_events, uninstall_integration ): """Assert concurrently evaluated flags do not pollute one another.""" - asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + asyncio = pytest.importorskip("asyncio") td = TestData.data_source() client = LDClient(config=Config("sdk-key", update_processor_class=td)) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index 4c581dff0b..c180211c3f 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -1,4 +1,6 @@ import concurrent.futures as cf +import sys + import pytest from openfeature import api @@ -91,12 +93,13 @@ def task(flag): } +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_openfeature_integration_asyncio( sentry_init, capture_events, uninstall_integration ): """Assert concurrently evaluated flags do not pollute one another.""" - asyncio = pytest.importorskip("asyncio") # Only available in Python 3.7+. + asyncio = pytest.importorskip("asyncio") uninstall_integration(OpenFeatureIntegration.identifier) sentry_init(integrations=[OpenFeatureIntegration()])