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
12 changes: 6 additions & 6 deletions api/api/urls/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@

from features.feature_states.views import (
delete_segment_override,
update_flag_v1,
update_flag_v2,
update_flag_option_a,
update_flag_option_b,
)

app_name = "experiments"

urlpatterns = [
path(
"environments/<str:environment_key>/update-flag-v1/",
update_flag_v1,
name="update-flag-v1",
update_flag_option_a,
name="update-flag-option-a",
),
path(
"environments/<str:environment_key>/update-flag-v2/",
update_flag_v2,
name="update-flag-v2",
update_flag_option_b,
name="update-flag-option-b",
),
path(
"environments/<str:environment_key>/delete-segment-override/",
Expand Down
4 changes: 2 additions & 2 deletions api/experimentation/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
)
from features.feature_states.serializers import (
FeatureValueSerializer,
MultivariateValueSerializer,
SegmentOverrideMultivariateValueSerializer,
)
from features.feature_types import MULTIVARIATE
from features.models import Feature
Expand Down Expand Up @@ -223,7 +223,7 @@ class ExperimentRolloutSerializer(serializers.Serializer): # type: ignore[type-
required=True, min_value=0, max_value=100
)
feature_state_value = FeatureValueSerializer(required=True)
multivariate_feature_state_values = MultivariateValueSerializer(
multivariate_feature_state_values = SegmentOverrideMultivariateValueSerializer(
many=True, required=False
)

Expand Down
45 changes: 25 additions & 20 deletions api/experimentation/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
)
from features.models import FeatureState
from features.value_types import BOOLEAN, INTEGER, STRING
from features.versioning.dataclasses import FlagChangeSet
from features.versioning.dataclasses import FeatureValue, FlagChangeSetOptionA
from features.versioning.versioning_service import (
update_flag,
update_flag_option_a,
update_multivariate_values,
)
from integrations.flagsmith.client import get_openfeature_client
Expand Down Expand Up @@ -594,26 +594,31 @@ def _get_live_rollout_override(experiment: Experiment) -> FeatureState | None:


def _update_live_feature_state(
feature_state: FeatureState, change_set: FlagChangeSet
feature_state: FeatureState, change_set: FlagChangeSetOptionA
) -> None:
feature_state.enabled = change_set.enabled
feature_state.save()
feature_state.feature_state_value.set_value(
change_set.feature_state_value, change_set.type_
)
feature_state.feature_state_value.save()
update_multivariate_values(feature_state, change_set.multivariate_values)
with transaction.atomic():
if change_set.enabled is not None:
feature_state.enabled = change_set.enabled
feature_state.save()
if change_set.value is not None:
feature_state.feature_state_value.set_value(
change_set.value.value, change_set.value.type_
)
feature_state.feature_state_value.save()
update_multivariate_values(feature_state, change_set.multivariate_values)


def _update_rollout_in_place(experiment: Experiment, change_set: FlagChangeSet) -> None:
def _update_rollout_in_place(
experiment: Experiment, change_set: FlagChangeSetOptionA
) -> None:
"""Write the rollout-segment override, keeping variant assignment stable.

Under v2 versioning, ``update_flag`` clones the override into a fresh feature
Under v2 versioning, ``update_flag_option_a`` clones the override into a fresh feature
state on every call. Since the multivariate split is salted on the feature
state id, that would re-randomise control/variant for already-enrolled
identities on each rollout update. Once the override exists, mutate it in
place instead (no version is published). Creating the override, and v1
versioning, still go through ``update_flag``, which already reuses the
versioning, still go through ``update_flag_option_a``, which already reuses the
feature state.

This is a temporary solution until we find a permanent fix for the
Expand All @@ -624,7 +629,7 @@ def _update_rollout_in_place(experiment: Experiment, change_set: FlagChangeSet)
):
_update_live_feature_state(override, change_set)
return
update_flag(experiment.environment, experiment.feature, change_set)
update_flag_option_a(experiment.environment, experiment.feature, change_set)


def apply_experiment_rollout(experiment: Experiment, spec: RolloutSpec) -> None:
Expand All @@ -638,11 +643,12 @@ def apply_experiment_rollout(experiment: Experiment, spec: RolloutSpec) -> None:
segment = _sync_rollout_segment(experiment, spec.rollout_percentage)
_update_rollout_in_place(
experiment,
FlagChangeSet(
FlagChangeSetOptionA(
author=spec.author,
enabled=spec.enabled,
feature_state_value=spec.feature_state_value,
type_=spec.value_type,
value=FeatureValue(
type_=spec.value_type, value=spec.feature_state_value
),
segment_id=segment.id,
multivariate_values=spec.multivariate_values,
),
Expand Down Expand Up @@ -699,11 +705,10 @@ def enable_experiment_rollout(experiment: Experiment, author: AuthorData) -> Non
value = rollout["feature_state_value"]
_update_rollout_in_place(
experiment,
FlagChangeSet(
FlagChangeSetOptionA(
author=author,
enabled=True,
feature_state_value=value["value"],
type_=value["type"],
value=FeatureValue(type_=value["type"], value=value["value"]),
segment_id=experiment.rollout_segment_id,
),
)
Expand Down
Loading
Loading