diff --git a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png
index c0d2a2179228e..8135c44c90209 100644
Binary files a/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png and b/frontend/__snapshots__/scenes-app-experiments--running-trend-experiment.png differ
diff --git a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png
index 485e90e0bfa80..373e3e0e19c98 100644
Binary files a/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--lifecycle-edit--webkit.png differ
diff --git a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
index 49107f8a6bbd3..2178b250c9dbc 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
@@ -323,6 +323,9 @@ export function FeatureFlagReleaseConditions({
return <>>
}
+ // TODO: EarlyAccessFeatureType is not the correct type for featureFlag.features, hence bypassing TS check
+ const hasMatchingEarlyAccessFeature = featureFlag.features?.find((f: any) => f.flagKey === featureFlag.key)
+
return (
{index > 0 && OR
}
@@ -365,6 +368,10 @@ export function FeatureFlagReleaseConditions({
- View Early Access Feature
+ {hasMatchingEarlyAccessFeature ? 'View Early Access Feature' : 'No Early Access Feature'}
diff --git a/posthog/api/organization_feature_flag.py b/posthog/api/organization_feature_flag.py
index 484b817a72de1..17b3e78a3b0cf 100644
--- a/posthog/api/organization_feature_flag.py
+++ b/posthog/api/organization_feature_flag.py
@@ -91,7 +91,9 @@ def copy_flags(self, request, *args, **kwargs):
"deleted": False,
}
- existing_flag = FeatureFlag.objects.filter(key=feature_flag_key, team_id=target_project_id).first()
+ existing_flag = FeatureFlag.objects.filter(
+ key=feature_flag_key, team_id=target_project_id, deleted=False
+ ).first()
# Update existing flag
if existing_flag:
feature_flag_serializer = FeatureFlagSerializer(
diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
index d1e736d1af3f4..7c515f351550e 100644
--- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
+++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr
@@ -1203,7 +1203,8 @@
"posthog_featureflag"."usage_dashboard_id",
"posthog_featureflag"."has_enriched_analytics"
FROM "posthog_featureflag"
- WHERE ("posthog_featureflag"."key" = 'copied-flag-key'
+ WHERE (NOT "posthog_featureflag"."deleted"
+ AND "posthog_featureflag"."key" = 'copied-flag-key'
AND "posthog_featureflag"."team_id" = 2)
ORDER BY "posthog_featureflag"."id" ASC
LIMIT 1 /*controller='organization_feature_flags-copy-flags',route='api/organizations/%28%3FP%3Cparent_lookup_organization_id%3E%5B%5E/.%5D%2B%29/feature_flags/copy_flags/%3F%24'*/
diff --git a/posthog/api/test/test_organization_feature_flag.py b/posthog/api/test/test_organization_feature_flag.py
index f1cd080acbc61..285f25f5a3149 100644
--- a/posthog/api/test/test_organization_feature_flag.py
+++ b/posthog/api/test/test_organization_feature_flag.py
@@ -1,6 +1,10 @@
from rest_framework import status
from posthog.models.team.team import Team
from posthog.models import FeatureFlag
+from posthog.models.experiment import Experiment
+from posthog.models.feedback.survey import Survey
+from posthog.models.early_access_feature import EarlyAccessFeature
+from posthog.api.dashboards.dashboard import Dashboard
from posthog.test.base import APIBaseTest, QueryMatchingTest, snapshot_postgres_queries
from typing import Any, Dict
@@ -130,7 +134,8 @@ def test_copy_feature_flag_create_new(self):
def test_copy_feature_flag_update_existing(self):
target_project = self.team_2
rollout_percentage_existing = 99
- self.feature_flag_existing = FeatureFlag.objects.create(
+
+ existing_flag = FeatureFlag.objects.create(
team=target_project,
created_by=self.user,
key=self.feature_flag_key,
@@ -140,6 +145,25 @@ def test_copy_feature_flag_update_existing(self):
ensure_experience_continuity=False,
)
+ # The following instances must remain linked to the existing flag after overwriting it
+ experiment = Experiment.objects.create(team=self.team_2, created_by=self.user, feature_flag_id=existing_flag.id)
+ survey = Survey.objects.create(team=self.team, created_by=self.user, linked_flag=existing_flag)
+ feature = EarlyAccessFeature.objects.create(
+ team=self.team,
+ feature_flag=existing_flag,
+ )
+ analytics_dashboard = Dashboard.objects.create(
+ team=self.team,
+ created_by=self.user,
+ )
+ existing_flag.analytics_dashboards.set([analytics_dashboard])
+ usage_dashboard = Dashboard.objects.create(
+ team=self.team,
+ created_by=self.user,
+ )
+ existing_flag.usage_dashboard = usage_dashboard
+ existing_flag.save()
+
url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags"
data = {
@@ -163,21 +187,112 @@ def test_copy_feature_flag_update_existing(self):
"rollout_percentage": self.rollout_percentage_to_copy,
"deleted": False,
"created_by": self.user.id,
+ "is_simple_flag": True,
+ "rollback_conditions": None,
+ "performed_rollback": False,
+ "can_edit": True,
+ "has_enriched_analytics": False,
+ "tags": [],
"id": "__ignore__",
"created_at": "__ignore__",
"usage_dashboard": "__ignore__",
+ "experiment_set": "__ignore__",
+ "surveys": "__ignore__",
+ "features": "__ignore__",
+ "analytics_dashboards": "__ignore__",
+ }
+
+ flag_response = response.json()["success"][0]
+
+ for key, expected_value in expected_flag_response.items():
+ self.assertIn(key, flag_response)
+ if expected_value != "__ignore__":
+ if key == "created_by":
+ self.assertEqual(flag_response[key]["id"], expected_value)
+ else:
+ self.assertEqual(flag_response[key], expected_value)
+
+ # Linked instances must remain linked
+ self.assertEqual(experiment.id, flag_response["experiment_set"][0])
+ self.assertEqual(str(survey.id), flag_response["surveys"][0]["id"])
+ self.assertEqual(str(feature.id), flag_response["features"][0]["id"])
+ self.assertEqual(analytics_dashboard.id, flag_response["analytics_dashboards"][0])
+ self.assertEqual(usage_dashboard.id, flag_response["usage_dashboard"])
+
+ self.assertSetEqual(
+ set(expected_flag_response.keys()),
+ set(flag_response.keys()),
+ )
+
+ def test_copy_feature_flag_update_override_deleted(self):
+ target_project = self.team_2
+ rollout_percentage_existing = 99
+
+ existing_deleted_flag = FeatureFlag.objects.create(
+ team=target_project,
+ created_by=self.user,
+ key=self.feature_flag_key,
+ name="Existing flag",
+ filters={"groups": [{"rollout_percentage": rollout_percentage_existing}]},
+ rollout_percentage=rollout_percentage_existing,
+ ensure_experience_continuity=False,
+ deleted=True,
+ )
+
+ # The following instances must be overriden for a soft-deleted flag
+ Experiment.objects.create(team=self.team_2, created_by=self.user, feature_flag_id=existing_deleted_flag.id)
+ Survey.objects.create(team=self.team, created_by=self.user, linked_flag=existing_deleted_flag)
+
+ analytics_dashboard = Dashboard.objects.create(
+ team=self.team,
+ created_by=self.user,
+ )
+ existing_deleted_flag.analytics_dashboards.set([analytics_dashboard])
+ usage_dashboard = Dashboard.objects.create(
+ team=self.team,
+ created_by=self.user,
+ )
+
+ existing_deleted_flag.usage_dashboard = usage_dashboard
+ existing_deleted_flag.save()
+
+ url = f"/api/organizations/{self.organization.id}/feature_flags/copy_flags"
+
+ data = {
+ "feature_flag_key": self.feature_flag_to_copy.key,
+ "from_project": self.feature_flag_to_copy.team_id,
+ "target_project_ids": [target_project.id],
+ }
+ response = self.client.post(url, data)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn("success", response.json())
+ self.assertIn("failed", response.json())
+
+ # Check copied flag in the response
+ expected_flag_response = {
+ "key": self.feature_flag_to_copy.key,
+ "name": self.feature_flag_to_copy.name,
+ "filters": self.feature_flag_to_copy.filters,
+ "active": self.feature_flag_to_copy.active,
+ "ensure_experience_continuity": self.feature_flag_to_copy.ensure_experience_continuity,
+ "rollout_percentage": self.rollout_percentage_to_copy,
+ "deleted": False,
+ "created_by": self.user.id,
"is_simple_flag": True,
- "experiment_set": [],
- "surveys": [],
- "features": [],
"rollback_conditions": None,
"performed_rollback": False,
"can_edit": True,
- "analytics_dashboards": [],
"has_enriched_analytics": False,
"tags": [],
+ "id": "__ignore__",
+ "created_at": "__ignore__",
+ "usage_dashboard": "__ignore__",
+ "experiment_set": "__ignore__",
+ "surveys": "__ignore__",
+ "features": "__ignore__",
+ "analytics_dashboards": "__ignore__",
}
-
flag_response = response.json()["success"][0]
for key, expected_value in expected_flag_response.items():
@@ -188,6 +303,12 @@ def test_copy_feature_flag_update_existing(self):
else:
self.assertEqual(flag_response[key], expected_value)
+ # Linked instances must be overriden for a soft-deleted flag
+ self.assertEqual(flag_response["experiment_set"], [])
+ self.assertEqual(flag_response["surveys"], [])
+ self.assertNotEqual(flag_response["usage_dashboard"], existing_deleted_flag.usage_dashboard.id)
+ self.assertEqual(flag_response["analytics_dashboards"], [])
+
self.assertSetEqual(
set(expected_flag_response.keys()),
set(flag_response.keys()),