Skip to content
Merged
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
41 changes: 38 additions & 3 deletions api/experimentation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,15 +285,50 @@ def create(self, validated_data: dict[str, Any]) -> Experiment:


class ExperimentFeatureSerializer(serializers.ModelSerializer): # type: ignore[type-arg]
multivariate_options = NestedMultivariateFeatureOptionSerializer(
many=True, read_only=True
)
multivariate_options = serializers.SerializerMethodField()

class Meta:
model = Feature
fields = ("id", "name", "type", "initial_value", "multivariate_options")
read_only_fields = fields

def get_multivariate_options(self, feature: Feature) -> list[dict[str, Any]]:
options = NestedMultivariateFeatureOptionSerializer(
feature.multivariate_options.all(), many=True
).data

environment: Environment | None = self.context.get("environment")
if not environment:
raise ValueError(
"ExperimentFeatureSerializer requires 'environment' in context."
)

env_state = (
feature.feature_states.filter(
environment=environment,
identity__isnull=True,
feature_segment__isnull=True,
Comment thread
Zaimwa9 marked this conversation as resolved.
)
.order_by("-live_from", "-version")
.first()
)
if not env_state:
Comment thread
Zaimwa9 marked this conversation as resolved.
raise ValueError(
f"No environment feature state found for feature {feature.id} "
f"in environment {environment.id}."
)

alloc_map = dict(
env_state.multivariate_feature_state_values.values_list(
"multivariate_feature_option_id", "percentage_allocation"
)
)
for option in options:
if option["id"] in alloc_map:
option["default_percentage_allocation"] = alloc_map[option["id"]]

return options # type: ignore[return-value]


class ExperimentListSerializer(ExperimentSerializer):
feature = ExperimentFeatureSerializer(read_only=True)
Expand Down
3 changes: 2 additions & 1 deletion api/experimentation/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def get_queryset(self) -> "QuerySet[Experiment]":
if self.action in ("list", "retrieve"):
qs = qs.select_related("feature").prefetch_related(
"feature__multivariate_options",
"feature__feature_states__multivariate_feature_state_values",
"experiment_metrics__metric",
)
status_filter = self.request.query_params.get("status")
Expand All @@ -189,7 +190,7 @@ def get_queryset(self) -> "QuerySet[Experiment]":
if q:
qs = qs.filter(Q(name__icontains=q) | Q(feature__name__icontains=q))

return qs
return qs.order_by("-created_at")

def list(self, request: Request, *args: object, **kwargs: object) -> Response:
response = super().list(request, *args, **kwargs)
Expand Down
70 changes: 69 additions & 1 deletion api/tests/unit/experimentation/test_experiment_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
ExperimentStatus,
Metric,
)
from experimentation.serializers import ExperimentFeatureSerializer
from features.feature_types import MULTIVARIATE
from features.models import Feature
from features.models import Feature, FeatureState
from features.multivariate.models import MultivariateFeatureStateValue
from tests.types import EnableFeaturesFixture

if TYPE_CHECKING:
Expand Down Expand Up @@ -1373,3 +1375,69 @@ def test_post__concurrent_create_race__returns_409(

# Then
assert response.status_code == status.HTTP_409_CONFLICT


def test_get_detail__env_level_allocations__returns_environment_percentages(
admin_client_new: APIClient,
environment: Environment,
experiment: Experiment,
multivariate_feature: Feature,
enable_features: EnableFeaturesFixture,
) -> None:
# Given
enable_features(EXPERIMENT_FLAG)
env_fs = FeatureState.objects.get(
feature=multivariate_feature,
environment=environment,
identity__isnull=True,
feature_segment__isnull=True,
)
env_allocations = [10.0, 20.0, 70.0]
for mv_fsv, alloc in zip(
MultivariateFeatureStateValue.objects.filter(feature_state=env_fs).order_by(
"multivariate_feature_option_id"
),
env_allocations,
):
mv_fsv.percentage_allocation = alloc
mv_fsv.save()

# When
response = admin_client_new.get(_detail_url(environment, experiment))

# Then
assert response.status_code == status.HTTP_200_OK
options = response.json()["feature"]["multivariate_options"]
returned_allocs = sorted(o["default_percentage_allocation"] for o in options)
assert returned_allocs == sorted(env_allocations)


def test_experiment_feature_serializer__no_environment_context__raises(
multivariate_feature: Feature,
) -> None:
# Given
serializer = ExperimentFeatureSerializer(multivariate_feature, context={})

# When / Then
with pytest.raises(ValueError, match="requires 'environment' in context"):
serializer.data


def test_experiment_feature_serializer__no_env_feature_state__raises(
environment: Environment,
multivariate_feature: Feature,
) -> None:
# Given
FeatureState.objects.filter(
feature=multivariate_feature,
environment=environment,
identity__isnull=True,
feature_segment__isnull=True,
).delete()
serializer = ExperimentFeatureSerializer(
multivariate_feature, context={"environment": environment}
)

# When / Then
with pytest.raises(ValueError, match="No environment feature state found"):
serializer.data
Loading