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 96dca6a5f291..8f9f3c0478bb 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.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/tests/unit/app_analytics/test_unit_app_analytics_views.py b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py index 4e1eef22539e..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 @@ -23,9 +23,15 @@ 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, @@ -59,6 +65,39 @@ def test_sdk_analytics_ignores_bad_data( ) +def test_sdk_analytics_ignores_feature_data_with_dots( + mocker: MockerFixture, + environment: Environment, + feature_with_dots: Feature, + api_client: APIClient, +) -> None: + # Given + api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) + + data = {feature_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=feature_with_dots.name, + evaluation_count=data[feature_with_dots.name], + labels={}, + ) + + def test_get_usage_data(mocker, admin_client, organisation): # type: ignore[no-untyped-def] # Given url = reverse("api-v1:organisations:usage-data", args=[organisation.id])