Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/features/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ class FeatureQuerySerializer(serializers.Serializer): # type: ignore[type-arg]
required=False,
help_text="Integer ID of the segment to retrieve segment overrides for.",
)
identity = serializers.IntegerField(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes ok, that's working when identities are stored in postgres, but when using edge identities, it passes an UUID.

I'll post a full comment with my findings as we'll need to have 2 implementations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you confirm you were having integer identity.id in the screenshot you shared ?

required=False,
help_text="Integer ID of the identity to sort features with identity overrides first.",
)
is_enabled = serializers.BooleanField(
allow_null=True,
required=False,
Expand Down
28 changes: 25 additions & 3 deletions api/features/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from common.projects.permissions import VIEW_PROJECT
from django.conf import settings
from django.core.cache import caches
from django.db.models import Max, Q, QuerySet
from django.db.models import Exists, Max, OuterRef, Q, QuerySet
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
Expand Down Expand Up @@ -56,7 +56,7 @@

from .constants import INTERSECTION, UNION
from .features_service import get_overrides_data
from .models import Feature, FeatureState
from .models import Feature, FeatureSegment, FeatureState
from .multivariate.serializers import (
FeatureMVOptionsValuesResponseSerializer,
)
Expand Down Expand Up @@ -224,7 +224,29 @@ def get_queryset(self): # type: ignore[no-untyped-def]
"-" if query_data["sort_direction"] == "DESC" else "",
query_data["sort_field"],
)
queryset = queryset.order_by(sort)
override_ordering: list[str] = []
if environment_id and (segment_id := query_data.get("segment")):
queryset = queryset.annotate(
has_segment_override=Exists(
FeatureSegment.objects.filter(
feature=OuterRef("pk"),
segment_id=segment_id,
environment_id=environment_id,
)
),
)
override_ordering.append("-has_segment_override")
if identity_id := query_data.get("identity"):
queryset = queryset.annotate(
has_identity_override=Exists(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here, this would make a subquery for each feature row right? I'm comparing to the segment query just above that have a unique_constraint on (feature, segment_id, environment_id), there might be a slight performance difference?
I personally think it's negligeable given that feature limit is 400 per project (~max overrides we can expect) but do you have an idea of the perf loss, how we could measaure it and at what point it could become problematic?

FeatureState.objects.filter(
feature=OuterRef("pk"),
identity_id=identity_id,
)
),
)
override_ordering.append("-has_identity_override")
queryset = queryset.order_by(*override_ordering, sort)

if environment_id:
page = self.paginate_queryset(queryset)
Expand Down
75 changes: 75 additions & 0 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4150,6 +4150,81 @@ def test_list_features_segment_query_param_with_invalid_segment(
assert feature_data["segment_feature_state"] is None


@pytest.mark.parametrize("sort_field", ["name", "created_date"])
def test_list_features__segment_query__sorts_by_field_with_overrides_first(
admin_client_new: APIClient,
project: Project,
environment: Environment,
segment: Segment,
sort_field: str,
) -> None:
# Given
Feature.objects.create(project=project, name="feature_a")
feature_b = Feature.objects.create(project=project, name="feature_b")
feature_c = Feature.objects.create(project=project, name="feature_c")
other_environment = Environment.objects.create(
project=project, name="other_environment"
)

# feature_c has a segment override in the requested environment
FeatureSegment.objects.create(
feature=feature_c, segment=segment, environment=environment
)
# feature_b has a segment override in a different environment
FeatureSegment.objects.create(
feature=feature_b, segment=segment, environment=other_environment
)

# When
response = admin_client_new.get(
f"/api/v1/projects/{project.id}/features/"
f"?environment={environment.id}"
f"&segment={segment.id}"
f"&sort_field={sort_field}&sort_direction=ASC"
)

# Then
assert response.status_code == status.HTTP_200_OK
results = response.json()["results"]
result_names = [r["name"] for r in results]
assert result_names[0] == "feature_c"
assert result_names[1:] == ["feature_a", "feature_b"]


@pytest.mark.parametrize("sort_field", ["name", "created_date"])
def test_list_features__identity_query__sorts_by_field_with_overrides_first(
admin_client_new: APIClient,
project: Project,
environment: Environment,
identity: Identity,
sort_field: str,
) -> None:
# Given
Feature.objects.create(project=project, name="feature_a")
Feature.objects.create(project=project, name="feature_b")
feature_c = Feature.objects.create(project=project, name="feature_c")

# feature_c has an identity override
FeatureState.objects.create(
feature=feature_c, environment=environment, identity=identity
)

# When
response = admin_client_new.get(
f"/api/v1/projects/{project.id}/features/"
f"?environment={environment.id}"
f"&identity={identity.id}"
f"&sort_field={sort_field}&sort_direction=ASC"
)

# Then
assert response.status_code == status.HTTP_200_OK
results = response.json()["results"]
result_names = [r["name"] for r in results]
assert result_names[0] == "feature_c"
assert result_names[1:] == ["feature_a", "feature_b"]


def test_create_multiple_features_with_metadata_keeps_metadata_isolated(
admin_client_new: APIClient,
project: Project,
Expand Down
8 changes: 4 additions & 4 deletions frontend/web/components/pages/UserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ const UserPage: FC = () => {
true,
search,
sort,
getServerFilter(filter),
{ ...getServerFilter(filter), identity: id },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we will need to use Utils.getIsEdge() to do something like ...(!isEdge ? { identity: id } : {}),, also for the next useEffect

)
}, [filter, environmentId, projectId])
}, [filter, environmentId, projectId, id])

useEffect(() => {
AppActions.getIdentity(environmentId, id)
Expand Down Expand Up @@ -141,10 +141,10 @@ const UserPage: FC = () => {
search,
sort,
pageNumber,
getServerFilter(filter),
{ ...getServerFilter(filter), identity: id },
)
},
[environmentId, projectId, filter],
[environmentId, projectId, filter, id],
)

return (
Expand Down
Loading