-
Notifications
You must be signed in to change notification settings - Fork 11
feat(analytics-flags): Add support for feature flag analytics #9
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 all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
763ab50
feat(flag-analytics): Add module to suppport flag analytics
gagantrivedi dbadf6a
feat(analytics-flag): Add analytics-flag to python sdk
gagantrivedi 7afa58b
refact(flag-analytics): Make analytics data async
gagantrivedi 8c221eb
fix typo
gagantrivedi 98b8159
squash! refac(analytics)
gagantrivedi 07a6bfd
minor refactoring and docs
gagantrivedi f36ae71
Add test module for analytics
gagantrivedi 2f43e2f
fix: mock call for py3.6-3.7
gagantrivedi 5345ede
refactor: remove data copy and other minor changes
gagantrivedi 2ddde6a
Fix casing of Content-Type header
52efd24
Tidy up docstrings
dcbe95e
fixup! add timeout
gagantrivedi 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import json | ||
| from datetime import datetime | ||
|
|
||
| from requests_futures.sessions import FuturesSession | ||
|
|
||
| ANALYTICS_ENDPOINT = "analytics/flags/" | ||
|
|
||
| # Used to control how often we send data(in seconds) | ||
| ANALYTICS_TIMER = 10 | ||
|
|
||
| session = FuturesSession(max_workers=4) | ||
|
|
||
|
|
||
| class AnalyticsProcessor: | ||
| """ | ||
| AnalyticsProcessor is used to track how often individual Flags are evaluated within | ||
| the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics. | ||
| """ | ||
|
|
||
| def __init__(self, environment_key: str, base_api_url: str, timeout: int = 3): | ||
| """ | ||
| Initialise the AnalyticsProcessor to handle sending analytics on flag usage to | ||
| the Flagsmith API. | ||
|
|
||
| :param environment_key: environment key obtained from the Flagsmith UI | ||
| :param base_api_url: base api url to override when using self hosted version | ||
| :param timeout: used to tell requests to stop waiting for a response after a | ||
| given number of seconds | ||
| """ | ||
| self.analytics_endpoint = base_api_url + ANALYTICS_ENDPOINT | ||
| self.environment_key = environment_key | ||
| self._last_flushed = datetime.now() | ||
| self.analytics_data = {} | ||
| self.timeout = timeout | ||
| super().__init__() | ||
|
|
||
| def flush(self): | ||
| """ | ||
| Sends all the collected data to the api asynchronously and resets the timer | ||
| """ | ||
|
|
||
| if not self.analytics_data: | ||
| return | ||
| session.post( | ||
| self.analytics_endpoint, | ||
| data=json.dumps(self.analytics_data), | ||
| timeout=self.timeout, | ||
| headers={ | ||
| "X-Environment-Key": self.environment_key, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| ) | ||
|
|
||
| self.analytics_data.clear() | ||
| self._last_flushed = datetime.now() | ||
|
|
||
| def track_feature(self, feature_id: int): | ||
| self.analytics_data[feature_id] = self.analytics_data.get(feature_id, 0) + 1 | ||
| if (datetime.now() - self._last_flushed).seconds > ANALYTICS_TIMER: | ||
| self.flush() | ||
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 |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| requests>=2.19.1 | ||
| requests>=2.19.1 | ||
| requests-futures==1.0.0 |
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,10 @@ | ||
| import pytest | ||
|
|
||
| from flagsmith.analytics import AnalyticsProcessor | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def analytics_processor(): | ||
| return AnalyticsProcessor( | ||
| environment_key="test_key", base_api_url="http://test_url" | ||
| ) |
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,63 @@ | ||
| import json | ||
| from datetime import datetime, timedelta | ||
| from unittest import mock | ||
|
|
||
| from flagsmith.analytics import ANALYTICS_TIMER, AnalyticsProcessor | ||
|
|
||
|
|
||
| def test_analytics_processor_track_feature_updates_analytics_data(analytics_processor): | ||
| # When | ||
| analytics_processor.track_feature(1) | ||
| assert analytics_processor.analytics_data[1] == 1 | ||
|
|
||
| analytics_processor.track_feature(1) | ||
| assert analytics_processor.analytics_data[1] == 2 | ||
|
|
||
|
|
||
| def test_analytics_processor_flush_clears_analytics_data(analytics_processor): | ||
| analytics_processor.track_feature(1) | ||
| analytics_processor.flush() | ||
| assert analytics_processor.analytics_data == {} | ||
|
|
||
|
|
||
| def test_analytics_processor_flush_post_request_data_match_ananlytics_data( | ||
| analytics_processor, | ||
| ): | ||
| # Given | ||
| with mock.patch("flagsmith.analytics.session") as session: | ||
matthewelwell marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # When | ||
| analytics_processor.track_feature(1) | ||
| analytics_processor.track_feature(2) | ||
| analytics_processor.flush() | ||
| # Then | ||
| session.post.assert_called() | ||
| post_call = session.mock_calls[0] | ||
| assert {"1": 1, "2": 1} == json.loads(post_call[2]["data"]) | ||
|
|
||
|
|
||
| def test_analytics_processor_flush_early_exit_if_analytics_data_is_empty( | ||
| analytics_processor, | ||
| ): | ||
| with mock.patch("flagsmith.analytics.session") as session: | ||
| analytics_processor.flush() | ||
|
|
||
| # Then | ||
| session.post.assert_not_called() | ||
|
|
||
|
|
||
| def test_analytics_processor_calling_track_feature_calls_flush_when_timer_runs_out( | ||
| analytics_processor, | ||
| ): | ||
| # Given | ||
| with mock.patch("flagsmith.analytics.datetime") as mocked_datetime, mock.patch( | ||
| "flagsmith.analytics.session" | ||
| ) as session: | ||
| # Let's move the time | ||
| mocked_datetime.now.return_value = datetime.now() + timedelta( | ||
| seconds=ANALYTICS_TIMER + 1 | ||
| ) | ||
| # When | ||
| analytics_processor.track_feature(1) | ||
|
|
||
| # Then | ||
| session.post.assert_called() | ||
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.