-
Notifications
You must be signed in to change notification settings - Fork 515
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(flags): Add integration for custom tracking of flag evaluations (#…
…3860) * Add new integration and unit tests * Test flag values for LD and OF threaded/asyncio, not just flag names * update ffIntegration test to be e2e, and fix LRU copy bug * make a helper fixture and test error processor in original thread * Move api to top-level, rename to add_flag * Add docstrs * Rename to add_feature_flag * Rm extra import in test_lru_cache * Revert lru comment * Type annotate * Review comments * Update launchdarkly and openfeature tests to be e2e * Update docstrs * Skip threading test for <3.7 * Skip ffs asyncio test if 3.6 * undo 'skip threading test' * Try commenting out asyncio * Use importorskip * Import order --------- Co-authored-by: Anton Pirker <[email protected]>
- Loading branch information
1 parent
4e69cb7
commit 50222ca
Showing
6 changed files
with
377 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
from sentry_sdk.flag_utils import flag_error_processor | ||
|
||
import sentry_sdk | ||
from sentry_sdk.integrations import Integration | ||
|
||
|
||
class FeatureFlagsIntegration(Integration): | ||
""" | ||
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. | ||
@example | ||
``` | ||
import sentry_sdk | ||
from sentry_sdk.integrations.featureflags import FeatureFlagsIntegration, add_feature_flag | ||
sentry_sdk.init(dsn="my_dsn", integrations=[FeatureFlagsIntegration()]); | ||
add_feature_flag('my-flag', true); | ||
sentry_sdk.capture_exception(Exception('broke')); // 'my-flag' should be captured on this Sentry event. | ||
``` | ||
""" | ||
|
||
identifier = "featureflags" | ||
|
||
@staticmethod | ||
def setup_once(): | ||
# type: () -> None | ||
scope = sentry_sdk.get_current_scope() | ||
scope.add_error_processor(flag_error_processor) | ||
|
||
|
||
def add_feature_flag(flag, result): | ||
# type: (str, bool) -> None | ||
""" | ||
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import concurrent.futures as cf | ||
import sys | ||
|
||
import pytest | ||
|
||
import sentry_sdk | ||
from sentry_sdk.integrations.featureflags import ( | ||
FeatureFlagsIntegration, | ||
add_feature_flag, | ||
) | ||
|
||
|
||
def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): | ||
uninstall_integration(FeatureFlagsIntegration.identifier) | ||
sentry_init(integrations=[FeatureFlagsIntegration()]) | ||
|
||
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!")) | ||
|
||
assert len(events) == 1 | ||
assert events[0]["contexts"]["flags"] == { | ||
"values": [ | ||
{"flag": "hello", "result": False}, | ||
{"flag": "world", "result": True}, | ||
{"flag": "other", "result": False}, | ||
] | ||
} | ||
|
||
|
||
def test_featureflags_integration_threaded( | ||
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) | ||
|
||
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!")) | ||
|
||
# Run tasks in separate threads | ||
with cf.ThreadPoolExecutor(max_workers=2) as pool: | ||
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": 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="requires python3.7 or higher") | ||
def test_featureflags_integration_asyncio( | ||
sentry_init, capture_events, uninstall_integration | ||
): | ||
asyncio = pytest.importorskip("asyncio") | ||
|
||
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}, | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.