Skip to content

Commit

Permalink
feat(experiments): Create experiment with existing feature flag (#28004)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
danielbachhuber and github-actions[bot] authored Feb 5, 2025
1 parent a7d9c97 commit 6a7387f
Show file tree
Hide file tree
Showing 7 changed files with 527 additions and 145 deletions.
75 changes: 48 additions & 27 deletions ee/clickhouse/views/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from posthog.clickhouse.query_tagging import tag_queries
from posthog.constants import INSIGHT_TRENDS
from posthog.models.experiment import Experiment, ExperimentHoldout, ExperimentSavedMetric
from posthog.models.feature_flag.feature_flag import FeatureFlag
from posthog.models.filters.filter import Filter
from posthog.utils import generate_cache_key, get_safe_cache

Expand Down Expand Up @@ -252,6 +253,19 @@ def validate_parameters(self, value):

return value

def validate_existing_feature_flag_for_experiment(self, feature_flag: FeatureFlag):
if feature_flag.experiment_set.exists():
raise ValidationError("Feature flag is already associated with an experiment.")

variants = feature_flag.filters.get("multivariate", {}).get("variants", [])

if len(variants) and len(variants) > 1:
if variants[0].get("key") != "control":
raise ValidationError("Feature flag must have control as the first variant.")
return True

raise ValidationError("Feature flag is not eligible for experiments.")

def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Experiment:
is_draft = "start_date" not in validated_data or validated_data["start_date"] is None

Expand All @@ -271,35 +285,42 @@ def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Experiment:

feature_flag_key = validated_data.pop("get_feature_flag_key")

holdout_groups = None
if validated_data.get("holdout"):
holdout_groups = validated_data["holdout"].filters

default_variants = [
{"key": "control", "name": "Control Group", "rollout_percentage": 50},
{"key": "test", "name": "Test Variant", "rollout_percentage": 50},
]

feature_flag_filters = {
"groups": [{"properties": [], "rollout_percentage": 100}],
"multivariate": {"variants": variants or default_variants},
"aggregation_group_type_index": aggregation_group_type_index,
"holdout_groups": holdout_groups,
}
existing_feature_flag = FeatureFlag.objects.filter(
key=feature_flag_key, team_id=self.context["team_id"], deleted=False
).first()
if existing_feature_flag:
self.validate_existing_feature_flag_for_experiment(existing_feature_flag)
feature_flag = existing_feature_flag
else:
holdout_groups = None
if validated_data.get("holdout"):
holdout_groups = validated_data["holdout"].filters

default_variants = [
{"key": "control", "name": "Control Group", "rollout_percentage": 50},
{"key": "test", "name": "Test Variant", "rollout_percentage": 50},
]

feature_flag_filters = {
"groups": [{"properties": [], "rollout_percentage": 100}],
"multivariate": {"variants": variants or default_variants},
"aggregation_group_type_index": aggregation_group_type_index,
"holdout_groups": holdout_groups,
}

feature_flag_serializer = FeatureFlagSerializer(
data={
"key": feature_flag_key,
"name": f'Feature Flag for Experiment {validated_data["name"]}',
"filters": feature_flag_filters,
"active": not is_draft,
"creation_context": "experiments",
},
context=self.context,
)
feature_flag_serializer = FeatureFlagSerializer(
data={
"key": feature_flag_key,
"name": f'Feature Flag for Experiment {validated_data["name"]}',
"filters": feature_flag_filters,
"active": not is_draft,
"creation_context": "experiments",
},
context=self.context,
)

feature_flag_serializer.is_valid(raise_exception=True)
feature_flag = feature_flag_serializer.save()
feature_flag_serializer.is_valid(raise_exception=True)
feature_flag = feature_flag_serializer.save()

if not validated_data.get("stats_config"):
validated_data["stats_config"] = {"version": 2}
Expand Down
80 changes: 55 additions & 25 deletions ee/clickhouse/views/test/test_clickhouse_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,31 +744,6 @@ def test_invalid_update(self):
"Can't update keys: get_feature_flag_key on Experiment",
)

def test_cant_reuse_existing_feature_flag(self):
ff_key = "a-b-test"
FeatureFlag.objects.create(
team=self.team,
rollout_percentage=50,
name="Beta feature",
key=ff_key,
created_by=self.user,
)
response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": "Test Experiment",
"description": "",
"start_date": "2021-12-01T10:23",
"end_date": None,
"feature_flag_key": ff_key,
"parameters": None,
"filters": {"events": []},
},
)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json()["detail"], "There is already a feature flag with this key.")

def test_draft_experiment_doesnt_have_FF_active(self):
# Draft experiment
ff_key = "a-b-tests"
Expand Down Expand Up @@ -1776,6 +1751,61 @@ def test_create_draft_experiment_without_filters(self) -> None:
self.assertEqual(response.json()["name"], "Test Experiment")
self.assertEqual(response.json()["feature_flag_key"], ff_key)

def test_create_experiment_with_feature_flag_missing_control(self):
feature_flag = FeatureFlag.objects.create(
team=self.team,
name="Beta feature",
key="beta-feature",
filters={
"multivariate": {
"variants": [
{"key": "test-1", "rollout_percentage": 50},
{"key": "test-2", "rollout_percentage": 50},
]
}
},
created_by=self.user,
)

response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": "Beta experiment",
"feature_flag_key": feature_flag.key,
"parameters": {},
},
)

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.json()["detail"], "Feature flag must have control as the first variant.")

def test_create_experiment_with_valid_existing_feature_flag(self):
feature_flag = FeatureFlag.objects.create(
team=self.team,
name="Beta feature",
key="beta-feature",
filters={
"multivariate": {
"variants": [
{"key": "control", "rollout_percentage": 50},
{"key": "test", "rollout_percentage": 50},
]
}
},
created_by=self.user,
)

response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": "Beta experiment",
"feature_flag_key": feature_flag.key,
"parameters": {},
},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.json()["feature_flag"]["id"], feature_flag.id)

def test_feature_flag_and_experiment_sync(self):
# Create an experiment with control and test variants
response = self.client.post(
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/lib/utils/eventUsageLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,8 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
experimentId,
metric,
}),
reportExperimentFeatureFlagModalOpened: () => ({}),
reportExperimentFeatureFlagSelected: (featureFlagKey: string) => ({ featureFlagKey }),
// Definition Popover
reportDataManagementDefinitionHovered: (type: TaxonomicFilterGroupType) => ({ type }),
reportDataManagementDefinitionClickView: (type: TaxonomicFilterGroupType) => ({ type }),
Expand Down Expand Up @@ -1041,6 +1043,12 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
reportExperimentMetricTimeout: ({ experimentId, metric }) => {
posthog.capture('experiment metric timeout', { experiment_id: experimentId, metric })
},
reportExperimentFeatureFlagModalOpened: () => {
posthog.capture('experiment feature flag modal opened')
},
reportExperimentFeatureFlagSelected: ({ featureFlagKey }: { featureFlagKey: string }) => {
posthog.capture('experiment feature flag selected', { feature_flag_key: featureFlagKey })
},
reportPropertyGroupFilterAdded: () => {
posthog.capture('property group filter added')
},
Expand Down
Loading

0 comments on commit 6a7387f

Please sign in to comment.