-
Notifications
You must be signed in to change notification settings - Fork 556
feat(flags): Add integration for custom tracking of flag evaluations #3860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
3652ce3
625969e
beb9512
296545d
921b133
4651b6a
36bc869
81c1761
42f76a3
ef01990
381ccc1
c76192e
b63982b
af1128c
4c6f08a
b4eb421
043e298
41fdde5
2dc679d
af9e92d
2cea37b
fea761c
752ce7d
d77ebc3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 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_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. We recommend you do this | ||
on flag evaluations. Flags are buffered per Sentry scope and limited to 100 per event. | ||
aliu39 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
flags = sentry_sdk.get_current_scope().flags | ||
flags.set(flag, result) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
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(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note on this fixture: I found using the Without using this, the error processor isn't added correctly. This isn't a concern for real use, since user apps only call |
||
"""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()]) | ||
|
||
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!")) | ||
[event] = events | ||
aliu39 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
assert event["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}, | ||
] | ||
} | ||
|
||
|
||
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}, | ||
] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.