Skip to content

Commit dcb0a37

Browse files
authored
feat(Features API): Restrict code references querying to a feature flag (#6970)
1 parent ec940fe commit dcb0a37

3 files changed

Lines changed: 64 additions & 26 deletions

File tree

api/features/views.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
from common.core.utils import is_database_replica_setup, using_database_replica
77
from common.projects.permissions import VIEW_PROJECT
88
from django.conf import settings
9+
from django.contrib.postgres.fields import ArrayField
910
from django.core.cache import caches
1011
from django.db.models import (
1112
BooleanField,
1213
Case,
1314
Exists,
15+
JSONField,
1416
Max,
1517
OuterRef,
1618
Q,
@@ -60,6 +62,7 @@
6062
NestedEnvironmentPermissions,
6163
)
6264
from features.value_types import BOOLEAN, INTEGER, STRING
65+
from integrations.flagsmith.client import get_client
6366
from projects.code_references.services import (
6467
annotate_feature_queryset_with_code_references_summary,
6568
)
@@ -217,9 +220,19 @@ def get_queryset(self): # type: ignore[no-untyped-def]
217220
query_serializer.is_valid(raise_exception=True)
218221
query_data = query_serializer.validated_data
219222

220-
queryset = annotate_feature_queryset_with_code_references_summary(
221-
queryset, project.id
223+
# TODO: Delete this after https://github.com/flagsmith/flagsmith/issues/6832 is resolved
224+
organisation = project.organisation
225+
flagsmith_client = get_client("local", local_eval=True)
226+
flags = flagsmith_client.get_identity_flags(
227+
organisation.flagsmith_identifier,
228+
traits=organisation.flagsmith_on_flagsmith_api_traits,
222229
)
230+
if flags.is_feature_enabled("code_references_ui_stats"):
231+
queryset = annotate_feature_queryset_with_code_references_summary(queryset)
232+
else:
233+
queryset = queryset.annotate(
234+
code_references_counts=Value([], output_field=ArrayField(JSONField()))
235+
)
223236

224237
queryset = self._filter_queryset(queryset, query_serializer)
225238

api/projects/code_references/services.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
from urllib.parse import urljoin
33

44
from django.contrib.postgres.expressions import ArraySubquery
5-
from django.contrib.postgres.fields import ArrayField
65
from django.db.models import (
76
BooleanField,
87
F,
98
Func,
10-
JSONField,
119
OuterRef,
1210
QuerySet,
1311
Subquery,
@@ -30,26 +28,13 @@
3028

3129
def annotate_feature_queryset_with_code_references_summary(
3230
queryset: QuerySet[Feature],
33-
project_id: int,
3431
) -> QuerySet[Feature]:
3532
"""Extend feature objects with a `code_references_counts`
3633
3734
NOTE: This adds compatibility with `CodeReferenceRepositoryCountSerializer`
3835
while preventing N+1 queries from the serializer.
3936
"""
4037
history_delta = timedelta(days=FEATURE_FLAG_CODE_REFERENCES_RETENTION_DAYS)
41-
cutoff_date = timezone.now() - history_delta
42-
43-
# Early exit: if no scans exist for this project, skip the expensive annotation
44-
has_scans = FeatureFlagCodeReferencesScan.objects.filter(
45-
project_id=project_id,
46-
created_at__gte=cutoff_date,
47-
).exists()
48-
49-
if not has_scans:
50-
return queryset.annotate(
51-
code_references_counts=Value([], output_field=ArrayField(JSONField()))
52-
)
5338
last_feature_found_at = (
5439
FeatureFlagCodeReferencesScan.objects.annotate(
5540
feature_name=OuterRef("feature_name"),

api/tests/unit/features/test_unit_features_views.py

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from projects.tags.models import Tag
6363
from segments.models import Segment
6464
from tests.types import (
65+
EnableFeaturesFixture,
6566
WithEnvironmentPermissionsCallable,
6667
WithProjectPermissionsCallable,
6768
)
@@ -3593,14 +3594,15 @@ def test_list_features__value_search_boolean__returns_matching(
35933594

35943595

35953596
def test_list_features__with_code_references__returns_counts(
3596-
staff_client: APIClient,
3597-
project: Project,
3597+
enable_features: EnableFeaturesFixture,
35983598
feature: Feature,
3599+
project: Project,
3600+
staff_client: APIClient,
35993601
with_project_permissions: WithProjectPermissionsCallable,
3600-
environment: Environment,
36013602
) -> None:
36023603
# Given
36033604
with_project_permissions([VIEW_PROJECT]) # type: ignore[call-arg]
3605+
enable_features("code_references_ui_stats")
36043606
with freeze_time("2099-01-01T10:00:00-0300"):
36053607
FeatureFlagCodeReferencesScan.objects.create(
36063608
project=project,
@@ -3678,26 +3680,64 @@ def test_list_features__with_code_references__returns_counts(
36783680
]
36793681

36803682

3681-
def test_FeatureViewSet_list__no_scans__returns_empty_code_references_counts(
3682-
staff_client: APIClient,
3683+
@pytest.mark.usefixtures("feature")
3684+
def test_list_features__without_code_references__returns_empty_counts(
3685+
enable_features: EnableFeaturesFixture,
3686+
environment: Environment,
36833687
project: Project,
3684-
feature: Feature,
3688+
staff_client: APIClient,
3689+
with_project_permissions: WithProjectPermissionsCallable,
3690+
) -> None:
3691+
# Given
3692+
with_project_permissions([VIEW_PROJECT]) # type: ignore[call-arg]
3693+
enable_features("code_references_ui_stats")
3694+
3695+
# When
3696+
response = staff_client.get(
3697+
f"/api/v1/projects/{project.id}/features/?environment={environment.id}"
3698+
)
3699+
3700+
# Then
3701+
assert response.status_code == 200
3702+
results = response.json()["results"]
3703+
assert len(results) == 1
3704+
assert results[0]["code_references_counts"] == []
3705+
3706+
3707+
# TODO: Delete this after https://github.com/flagsmith/flagsmith/issues/6832 is resolved
3708+
def test_list_features__code_references_ui_stats_disabled__returns_empty_counts(
3709+
enable_features: EnableFeaturesFixture,
36853710
environment: Environment,
3711+
feature: Feature,
3712+
project: Project,
3713+
staff_client: APIClient,
36863714
with_project_permissions: WithProjectPermissionsCallable,
36873715
) -> None:
3688-
# Given - project has no code reference scans
3716+
# Given
36893717
with_project_permissions([VIEW_PROJECT]) # type: ignore[call-arg]
3718+
enable_features() # code_references_ui_stats not enabled
3719+
FeatureFlagCodeReferencesScan.objects.create(
3720+
project=project,
3721+
repository_url="https://github.flagsmith.com/backend/",
3722+
revision="rev-1",
3723+
code_references=[
3724+
{
3725+
"feature_name": feature.name,
3726+
"file_path": "path/to/file.py",
3727+
"line_number": 42,
3728+
},
3729+
],
3730+
)
36903731

36913732
# When
36923733
response = staff_client.get(
36933734
f"/api/v1/projects/{project.id}/features/?environment={environment.id}"
36943735
)
36953736

3696-
# Then - response should include code_references_counts as empty list
3737+
# Then
36973738
assert response.status_code == 200
36983739
results = response.json()["results"]
36993740
assert len(results) == 1
3700-
assert "code_references_counts" in results[0]
37013741
assert results[0]["code_references_counts"] == []
37023742

37033743

0 commit comments

Comments
 (0)