From 379afd37406972be433b53e8232f37e609f43c9b Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Thu, 23 Oct 2025 23:43:02 +0200 Subject: [PATCH 1/4] fix: parsing for features with . --- api/app_analytics/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/app_analytics/views.py b/api/app_analytics/views.py index 96dca6a5f291..bead8f266d20 100644 --- a/api/app_analytics/views.py +++ b/api/app_analytics/views.py @@ -88,7 +88,9 @@ def get_fields(self): # type: ignore[no-untyped-def] def save(self, **kwargs: typing.Any) -> None: request = self.context["request"] - for feature_name, evaluation_count in self.validated_data.items(): + # validated_data splits out request body with '.' in feature name (e.g a.b.c). + # Instead, it's safe to use self.initial_data as keys are not altered. + for feature_name, evaluation_count in self.initial_data.items(): feature_evaluation_cache.track_feature_evaluation( environment_id=request.environment.id, feature_name=feature_name, From 99b35dcd666550f664ca8e21c27efb2b65e45593 Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Fri, 24 Oct 2025 17:46:13 +0200 Subject: [PATCH 2/4] fix: use data instead and add test --- api/.gitignore | 1 + api/app_analytics/views.py | 4 +-- api/conftest.py | 4 +++ .../test_unit_app_analytics_views.py | 32 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/api/.gitignore b/api/.gitignore index f1af0ea21de0..e7b97e4e290f 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -8,3 +8,4 @@ features/workflows/logic/ # Unit test coverage .coverage +.coverage.* diff --git a/api/app_analytics/views.py b/api/app_analytics/views.py index bead8f266d20..8f9f3c0478bb 100644 --- a/api/app_analytics/views.py +++ b/api/app_analytics/views.py @@ -89,8 +89,8 @@ def get_fields(self): # type: ignore[no-untyped-def] def save(self, **kwargs: typing.Any) -> None: request = self.context["request"] # validated_data splits out request body with '.' in feature name (e.g a.b.c). - # Instead, it's safe to use self.initial_data as keys are not altered. - for feature_name, evaluation_count in self.initial_data.items(): + # Instead, it's safe to use self.data as keys are not altered. + for feature_name, evaluation_count in self.data.items(): feature_evaluation_cache.track_feature_evaluation( environment_id=request.environment.id, feature_name=feature_name, diff --git a/api/conftest.py b/api/conftest.py index 3961913d9d08..7ea7d1880dd4 100644 --- a/api/conftest.py +++ b/api/conftest.py @@ -555,6 +555,10 @@ def api_client() -> APIClient: def feature(project: Project) -> Feature: return Feature.objects.create(name="Test Feature1", project=project) # type: ignore[no-any-return] +@pytest.fixture() +def feauture_with_dots(project: Project) -> Feature: + return Feature.objects.create(name="feature.name", project=project) # type: ignore[no-any-return] + @pytest.fixture() def change_request(environment: Environment, admin_user: FFAdminUser) -> ChangeRequest: diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py index 4e1eef22539e..07b154182b64 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py @@ -58,6 +58,38 @@ def test_sdk_analytics_ignores_bad_data( labels={}, ) +def test_sdk_analytics_ignores_feature_data_with_dots( + mocker: MockerFixture, + environment: Environment, + feauture_with_dots: Feature, + api_client: APIClient, +) -> None: + # Given + api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) + + data = { feauture_with_dots.name: 20 } + mocked_feature_eval_cache = mocker.patch( + "app_analytics.views.feature_evaluation_cache" + ) + + url = reverse("api-v1:analytics-flags") + + # When + response = api_client.post( + url, data=json.dumps(data), content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_200_OK + assert mocked_feature_eval_cache.track_feature_evaluation.call_count == 1 + + mocked_feature_eval_cache.track_feature_evaluation.assert_called_once_with( + environment_id=environment.id, + feature_name=feauture_with_dots.name, + evaluation_count=data[feauture_with_dots.name], + labels={}, + ) + def test_get_usage_data(mocker, admin_client, organisation): # type: ignore[no-untyped-def] # Given From 2221473cfe87e1ee66494a4714ea6d499c05a48a Mon Sep 17 00:00:00 2001 From: existentialcoder Date: Tue, 28 Oct 2025 15:09:55 +0100 Subject: [PATCH 3/4] fix: address review comment --- api/conftest.py | 4 ---- .../app_analytics/test_unit_app_analytics_views.py | 12 ++++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/conftest.py b/api/conftest.py index 7ea7d1880dd4..3961913d9d08 100644 --- a/api/conftest.py +++ b/api/conftest.py @@ -555,10 +555,6 @@ def api_client() -> APIClient: def feature(project: Project) -> Feature: return Feature.objects.create(name="Test Feature1", project=project) # type: ignore[no-any-return] -@pytest.fixture() -def feauture_with_dots(project: Project) -> Feature: - return Feature.objects.create(name="feature.name", project=project) # type: ignore[no-any-return] - @pytest.fixture() def change_request(environment: Environment, admin_user: FFAdminUser) -> ChangeRequest: diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py index 07b154182b64..70403c54f595 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py @@ -18,6 +18,7 @@ from app_analytics.models import FeatureEvaluationRaw from environments.identities.models import Identity from environments.models import Environment +from projects.models import Project from features.models import Feature from organisations.models import ( Organisation, @@ -25,6 +26,9 @@ ) from tests.types import EnableFeaturesFixture +@pytest.fixture() +def feature_with_dots(project: Project) -> Feature: + return Feature.objects.create(name="feature.with.dots", project=project) # type: ignore[no-any-return] def test_sdk_analytics_ignores_bad_data( mocker: MockerFixture, @@ -61,13 +65,13 @@ def test_sdk_analytics_ignores_bad_data( def test_sdk_analytics_ignores_feature_data_with_dots( mocker: MockerFixture, environment: Environment, - feauture_with_dots: Feature, + feature_with_dots: Feature, api_client: APIClient, ) -> None: # Given api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) - data = { feauture_with_dots.name: 20 } + data = { feature_with_dots.name: 20 } mocked_feature_eval_cache = mocker.patch( "app_analytics.views.feature_evaluation_cache" ) @@ -85,8 +89,8 @@ def test_sdk_analytics_ignores_feature_data_with_dots( mocked_feature_eval_cache.track_feature_evaluation.assert_called_once_with( environment_id=environment.id, - feature_name=feauture_with_dots.name, - evaluation_count=data[feauture_with_dots.name], + feature_name=feature_with_dots.name, + evaluation_count=data[feature_with_dots.name], labels={}, ) From eea586bbbf3a46dc0e99a3ea232c80f0f314a95a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:10:46 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../unit/app_analytics/test_unit_app_analytics_views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py index 70403c54f595..7591c6cb2752 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py @@ -18,18 +18,20 @@ from app_analytics.models import FeatureEvaluationRaw from environments.identities.models import Identity from environments.models import Environment -from projects.models import Project from features.models import Feature from organisations.models import ( Organisation, OrganisationSubscriptionInformationCache, ) +from projects.models import Project from tests.types import EnableFeaturesFixture + @pytest.fixture() def feature_with_dots(project: Project) -> Feature: return Feature.objects.create(name="feature.with.dots", project=project) # type: ignore[no-any-return] + def test_sdk_analytics_ignores_bad_data( mocker: MockerFixture, environment: Environment, @@ -62,6 +64,7 @@ def test_sdk_analytics_ignores_bad_data( labels={}, ) + def test_sdk_analytics_ignores_feature_data_with_dots( mocker: MockerFixture, environment: Environment, @@ -71,7 +74,7 @@ def test_sdk_analytics_ignores_feature_data_with_dots( # Given api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) - data = { feature_with_dots.name: 20 } + data = {feature_with_dots.name: 20} mocked_feature_eval_cache = mocker.patch( "app_analytics.views.feature_evaluation_cache" )