-
Notifications
You must be signed in to change notification settings - Fork 570
feat(flags): add Statsig integration #4022
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
Merged
Merged
Changes from 1 commit
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
a19ac46
feat(flags): add Statsig integration
aliu39 5603d1a
Merge branch 'master' into aliu/statsig
aliu39 db4bdfb
Use helper
aliu39 7ba0e84
Try regular imports
aliu39 24cffd7
add typing_extensions to test reqs
sentrivana 1b25aa7
Merge branch 'master' into aliu/statsig
sentrivana d3fef9a
fix version no
sentrivana 07aa45a
add min version check
sentrivana bcbd423
ok we actually need typing_extensions explicitly
sentrivana 293930a
add to toxgen script
sentrivana f2ca15d
Merge branch 'master' into aliu/statsig
sentrivana File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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 |
|---|---|---|
|
|
@@ -87,6 +87,7 @@ | |
| "Flags": [ | ||
| "launchdarkly", | ||
| "openfeature", | ||
| "statsig", | ||
| "unleash", | ||
| ], | ||
| "Gevent": [ | ||
|
|
||
This file contains hidden or 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,39 @@ | ||
| from functools import wraps | ||
| from typing import Any, TYPE_CHECKING | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk.integrations import Integration, DidNotEnable | ||
|
|
||
| import importlib | ||
|
|
||
| try: | ||
| # The statsig package has the same name as this file. We use importlib to avoid conflicts. | ||
| statsig = importlib.import_module("statsig.statsig") | ||
| except ImportError: | ||
| raise DidNotEnable("statsig is not installed") | ||
|
|
||
| if TYPE_CHECKING: | ||
| statsig_user = importlib.import_module("statsig.statsig_user") | ||
| StatsigUser = statsig_user.StatsigUser | ||
|
|
||
|
|
||
| class StatsigIntegration(Integration): | ||
| identifier = "statsig" | ||
|
|
||
| @staticmethod | ||
| def setup_once(): | ||
| # type: () -> None | ||
| # Wrap and patch evaluation method(s) in the statsig module | ||
| old_check_gate = statsig.check_gate | ||
|
|
||
| @wraps(old_check_gate) | ||
| def sentry_check_gate(user, gate, *args, **kwargs): | ||
| # type: (StatsigUser, str, *Any, **Any) -> Any | ||
| enabled = old_check_gate(user, gate, *args, **kwargs) | ||
|
|
||
| flags = sentry_sdk.get_current_scope().flags | ||
| flags.set(gate, enabled) | ||
aliu39 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return enabled | ||
|
|
||
| statsig.check_gate = sentry_check_gate | ||
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,3 @@ | ||
| import pytest | ||
|
|
||
| pytest.importorskip("statsig") |
This file contains hidden or 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,183 @@ | ||
| import concurrent.futures as cf | ||
| import sys | ||
| from contextlib import contextmanager | ||
| from statsig import statsig | ||
| from statsig.statsig_user import StatsigUser | ||
| from random import random | ||
| from unittest.mock import Mock | ||
|
|
||
| import pytest | ||
|
|
||
| import sentry_sdk | ||
| from sentry_sdk.integrations.statsig import StatsigIntegration | ||
|
|
||
|
|
||
| @contextmanager | ||
| def mock_statsig(gate_dict): | ||
| old_check_gate = statsig.check_gate | ||
|
|
||
| def mock_check_gate(user, gate, *args, **kwargs): | ||
| return gate_dict.get(gate, False) | ||
|
|
||
| statsig.check_gate = Mock(side_effect=mock_check_gate) | ||
|
|
||
| yield | ||
|
|
||
| statsig.check_gate = old_check_gate | ||
|
|
||
|
|
||
| def test_check_gate(sentry_init, capture_events, uninstall_integration): | ||
| uninstall_integration(StatsigIntegration.identifier) | ||
|
|
||
| with mock_statsig({"hello": True, "world": False}): | ||
| sentry_init(integrations=[StatsigIntegration()]) | ||
| events = capture_events() | ||
| user = StatsigUser(user_id="user-id") | ||
|
|
||
| statsig.check_gate(user, "hello") | ||
| statsig.check_gate(user, "world") | ||
| statsig.check_gate(user, "other") # unknown gates default to False. | ||
|
|
||
| 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": False}, | ||
| ] | ||
| } | ||
|
|
||
|
|
||
| def test_check_gate_threaded(sentry_init, capture_events, uninstall_integration): | ||
| uninstall_integration(StatsigIntegration.identifier) | ||
|
|
||
| with mock_statsig({"hello": True, "world": False}): | ||
| sentry_init(integrations=[StatsigIntegration()]) | ||
| events = capture_events() | ||
| user = StatsigUser(user_id="user-id") | ||
|
|
||
| # Capture an eval before we split isolation scopes. | ||
| statsig.check_gate(user, "hello") | ||
|
|
||
| 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(): | ||
| statsig.check_gate(user, flag_key) | ||
| # 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!")) | ||
|
|
||
| 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": 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}, | ||
| ] | ||
| } | ||
|
|
||
|
|
||
| @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") | ||
| def test_check_gate_asyncio(sentry_init, capture_events, uninstall_integration): | ||
| asyncio = pytest.importorskip("asyncio") | ||
| uninstall_integration(StatsigIntegration.identifier) | ||
|
|
||
| with mock_statsig({"hello": True, "world": False}): | ||
| sentry_init(integrations=[StatsigIntegration()]) | ||
| events = capture_events() | ||
| user = StatsigUser(user_id="user-id") | ||
|
|
||
| # Capture an eval before we split isolation scopes. | ||
| statsig.check_gate(user, "hello") | ||
|
|
||
| async def task(flag_key): | ||
| with sentry_sdk.isolation_scope(): | ||
| statsig.check_gate(user, flag_key) | ||
| # 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": 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_wraps_original(sentry_init, uninstall_integration): | ||
| uninstall_integration(StatsigIntegration.identifier) | ||
| flag_value = random() < 0.5 | ||
|
|
||
| with mock_statsig( | ||
| {"test-flag": flag_value} | ||
| ): # patches check_gate with a Mock object. | ||
| mock_check_gate = statsig.check_gate | ||
| sentry_init(integrations=[StatsigIntegration()]) # wraps check_gate. | ||
| user = StatsigUser(user_id="user-id") | ||
|
|
||
| res = statsig.check_gate(user, "test-flag", "extra-arg", kwarg=1) # type: ignore[arg-type] | ||
|
|
||
| assert res == flag_value | ||
| assert mock_check_gate.call_args == ( # type: ignore[attr-defined] | ||
| (user, "test-flag", "extra-arg"), | ||
| {"kwarg": 1}, | ||
| ) | ||
|
|
||
|
|
||
| def test_wrapper_attributes(sentry_init, uninstall_integration): | ||
| uninstall_integration(StatsigIntegration.identifier) | ||
| original_check_gate = statsig.check_gate | ||
| sentry_init(integrations=[StatsigIntegration()]) | ||
|
|
||
| # Methods have not lost their qualified names after decoration. | ||
| assert statsig.check_gate.__name__ == "check_gate" | ||
| assert statsig.check_gate.__qualname__ == original_check_gate.__qualname__ | ||
|
|
||
| # Clean up | ||
| statsig.check_gate = original_check_gate |
This file contains hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.