diff --git a/frontend/src/scenes/feature-flags/FeatureFlag.tsx b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
index 87d1ed2ef1a10..5fbc1adeb567c 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlag.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlag.tsx
@@ -172,6 +172,14 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
})
}
+ if (featureFlags[FEATURE_FLAGS.SCHEDULED_CHANGES_FEATURE_FLAGS]) {
+ tabs.push({
+ label: 'Schedule',
+ key: FeatureFlagsTab.SCHEDULE,
+ content: ,
+ })
+ }
+
if (featureFlags[FEATURE_FLAGS.FF_DASHBOARD_TEMPLATES] && featureFlag.key && id) {
tabs.push({
label: (
@@ -222,14 +230,6 @@ export function FeatureFlag({ id }: { id?: string } = {}): JSX.Element {
})
}
- if (featureFlags[FEATURE_FLAGS.SCHEDULED_CHANGES_FEATURE_FLAGS]) {
- tabs.push({
- label: 'Schedule',
- key: FeatureFlagsTab.SCHEDULE,
- content: ,
- })
- }
-
return (
<>
diff --git a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
index a3231e269c30d..16abb06bb344d 100644
--- a/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
+++ b/frontend/src/scenes/feature-flags/FeatureFlagReleaseConditions.tsx
@@ -418,7 +418,7 @@ export function FeatureFlagReleaseConditions({
)}
- {!readOnly && showGroupsOptions && (
+ {!readOnly && showGroupsOptions && usageContext !== 'schedule' && (
Match by
Date and time
{
+ const now = new Date()
+ return dateMarker.toDate().getTime() < now.getTime()
+ }}
value={scheduleDateMarker}
onChange={(value) => setScheduleDateMarker(value)}
className="h-10 w-60"
@@ -109,12 +177,13 @@ export default function FeatureFlagSchedule(): JSX.Element {
showTime
showSecond={false}
format={DAYJS_FORMAT}
+ showNow={false}
/>
- {scheduledChangeField === 'active' && (
+ {scheduledChangeOperation === ScheduledChangeOperationType.UpdateStatus && (
<>
>
)}
- {scheduledChangeField === 'filters' && }
+ {scheduledChangeOperation === ScheduledChangeOperationType.AddReleaseCondition && (
+
+ )}
(record.executed_at ? 'opacity-75' : '')}
className="mt-8"
loading={false}
dataSource={scheduledChanges}
diff --git a/frontend/src/scenes/feature-flags/featureFlagLogic.ts b/frontend/src/scenes/feature-flags/featureFlagLogic.ts
index bdd313aba8281..c18d323f1fc10 100644
--- a/frontend/src/scenes/feature-flags/featureFlagLogic.ts
+++ b/frontend/src/scenes/feature-flags/featureFlagLogic.ts
@@ -42,6 +42,7 @@ import {
PropertyFilterType,
PropertyOperator,
RolloutConditionType,
+ ScheduledChangeOperationType,
ScheduledChangeType,
Survey,
SurveyQuestionType,
@@ -233,7 +234,7 @@ export const featureFlagLogic = kea([
setFeatureFlagId: (id: number | null) => ({ id }),
setCopyDestinationProject: (id: number | null) => ({ id }),
setScheduleDateMarker: (dateMarker: any) => ({ dateMarker }),
- setScheduledChangeField: (changeType: string | null) => ({ changeType }),
+ setScheduledChangeOperation: (changeType: string | null) => ({ changeType }),
}),
forms(({ actions, values }) => ({
featureFlag: {
@@ -493,10 +494,10 @@ export const featureFlagLogic = kea([
setScheduleDateMarker: (_, { dateMarker }) => dateMarker,
},
],
- scheduledChangeField: [
- 'active' as string | null,
+ scheduledChangeOperation: [
+ ScheduledChangeOperationType.AddReleaseCondition as string | null,
{
- setScheduledChangeField: (_, { changeType }) => changeType,
+ setScheduledChangeOperation: (_, { changeType }) => changeType,
},
],
}),
@@ -657,16 +658,20 @@ export const featureFlagLogic = kea([
scheduledChange: {
__default: {} as ScheduledChangeType,
createScheduledChange: async () => {
- const { featureFlag, scheduledChangeField, scheduleDateMarker, currentTeamId } = values
+ const { featureFlag, scheduledChangeOperation, scheduleDateMarker, currentTeamId } = values
- if (currentTeamId && scheduledChangeField) {
+ const fields = {
+ [ScheduledChangeOperationType.UpdateStatus]: 'active',
+ [ScheduledChangeOperationType.AddReleaseCondition]: 'filters',
+ }
+
+ if (currentTeamId && scheduledChangeOperation) {
const data = {
record_id: values.featureFlag.id,
model_name: 'FeatureFlag',
- operation: scheduledChangeField,
payload: {
- field: scheduledChangeField,
- value: featureFlag[scheduledChangeField],
+ operation: scheduledChangeOperation,
+ value: featureFlag[fields[scheduledChangeOperation]],
},
scheduled_at: scheduleDateMarker.toISOString(),
}
@@ -873,6 +878,11 @@ export const featureFlagLogic = kea([
if (scheduledChange && scheduledChange) {
lemonToast.success('Change scheduled successfully')
actions.loadScheduledChanges()
+ actions.setFeatureFlag({
+ ...values.featureFlag,
+ filters: NEW_FLAG.filters,
+ active: NEW_FLAG.active,
+ })
}
},
deleteScheduledChangeSuccess: ({ scheduledChange }) => {
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index efdb944d97756..71137a245b67e 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -2449,12 +2449,21 @@ export enum ScheduledChangeModels {
FeatureFlag = 'FeatureFlag',
}
+export enum ScheduledChangeOperationType {
+ UpdateStatus = 'update_status',
+ AddReleaseCondition = 'add_release_condition',
+}
+
+export type ScheduledChangePayload =
+ | { operation: ScheduledChangeOperationType.UpdateStatus; value: boolean }
+ | { operation: ScheduledChangeOperationType.AddReleaseCondition; value: FeatureFlagFilters }
+
export interface ScheduledChangeType {
id: number
team_id: number
record_id: number | string
model_name: ScheduledChangeModels
- payload: Record
+ payload: ScheduledChangePayload
scheduled_at: string
executed_at: string | null
failure_reason: string | null
diff --git a/posthog/models/feature_flag/feature_flag.py b/posthog/models/feature_flag/feature_flag.py
index 80c92452d6d74..8af48dd6f124d 100644
--- a/posthog/models/feature_flag/feature_flag.py
+++ b/posthog/models/feature_flag/feature_flag.py
@@ -1,4 +1,5 @@
import json
+from django.http import HttpRequest
import structlog
from typing import Dict, List, Optional, cast
@@ -303,16 +304,21 @@ def scheduled_changes_dispatcher(self, payload):
if "operation" not in payload or "value" not in payload:
raise Exception("Invalid payload")
+ http_request = HttpRequest()
+ http_request.user = self.created_by
context = {
- "request": {"user": self.created_by},
+ "request": http_request,
"team_id": self.team_id,
}
+
serializer_data = {}
if payload["operation"] == "add_release_condition":
- existing_groups = self.get_filters().get("groups", [])
+ current_filters = self.get_filters()
+ current_groups = current_filters.get("groups", [])
new_groups = payload["value"].get("groups", [])
- serializer_data["filters"] = {"groups": existing_groups + new_groups}
+
+ serializer_data["filters"] = {**current_filters, "groups": current_groups + new_groups}
elif payload["operation"] == "update_status":
serializer_data["active"] = payload["value"]
else:
diff --git a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr
index 87019fd274336..e0f024e21038f 100644
--- a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr
+++ b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr
@@ -44,6 +44,88 @@
'
---
# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.10
+ '
+ SELECT "posthog_organization"."id",
+ "posthog_organization"."name",
+ "posthog_organization"."slug",
+ "posthog_organization"."created_at",
+ "posthog_organization"."updated_at",
+ "posthog_organization"."plugins_access_level",
+ "posthog_organization"."for_internal_metrics",
+ "posthog_organization"."is_member_join_email_enabled",
+ "posthog_organization"."enforce_2fa",
+ "posthog_organization"."customer_id",
+ "posthog_organization"."available_product_features",
+ "posthog_organization"."usage",
+ "posthog_organization"."never_drop_data",
+ "posthog_organization"."setup_section_2_completed",
+ "posthog_organization"."personalization",
+ "posthog_organization"."domain_whitelist",
+ "posthog_organization"."available_features"
+ FROM "posthog_organization"
+ WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.11
+ '
+ SELECT "posthog_team"."id",
+ "posthog_team"."uuid",
+ "posthog_team"."organization_id",
+ "posthog_team"."api_token",
+ "posthog_team"."app_urls",
+ "posthog_team"."name",
+ "posthog_team"."slack_incoming_webhook",
+ "posthog_team"."created_at",
+ "posthog_team"."updated_at",
+ "posthog_team"."anonymize_ips",
+ "posthog_team"."completed_snippet_onboarding",
+ "posthog_team"."has_completed_onboarding_for",
+ "posthog_team"."ingested_event",
+ "posthog_team"."autocapture_opt_out",
+ "posthog_team"."autocapture_exceptions_opt_in",
+ "posthog_team"."autocapture_exceptions_errors_to_ignore",
+ "posthog_team"."session_recording_opt_in",
+ "posthog_team"."session_recording_sample_rate",
+ "posthog_team"."session_recording_minimum_duration_milliseconds",
+ "posthog_team"."session_recording_linked_flag",
+ "posthog_team"."session_recording_network_payload_capture_config",
+ "posthog_team"."capture_console_log_opt_in",
+ "posthog_team"."capture_performance_opt_in",
+ "posthog_team"."surveys_opt_in",
+ "posthog_team"."session_recording_version",
+ "posthog_team"."signup_token",
+ "posthog_team"."is_demo",
+ "posthog_team"."access_control",
+ "posthog_team"."week_start_day",
+ "posthog_team"."inject_web_apps",
+ "posthog_team"."test_account_filters",
+ "posthog_team"."test_account_filters_default_checked",
+ "posthog_team"."path_cleaning_filters",
+ "posthog_team"."timezone",
+ "posthog_team"."data_attributes",
+ "posthog_team"."person_display_name_properties",
+ "posthog_team"."live_events_columns",
+ "posthog_team"."recording_domains",
+ "posthog_team"."primary_dashboard_id",
+ "posthog_team"."extra_settings",
+ "posthog_team"."correlation_config",
+ "posthog_team"."session_recording_retention_period_days",
+ "posthog_team"."plugins_opt_in",
+ "posthog_team"."opt_out_capture",
+ "posthog_team"."event_names",
+ "posthog_team"."event_names_with_usage",
+ "posthog_team"."event_properties",
+ "posthog_team"."event_properties_with_usage",
+ "posthog_team"."event_properties_numerical",
+ "posthog_team"."external_data_workspace_id",
+ "posthog_team"."external_data_workspace_last_synced_at"
+ FROM "posthog_team"
+ WHERE "posthog_team"."id" = 2
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.12
'
SELECT "posthog_scheduledchange"."id",
"posthog_scheduledchange"."record_id",
@@ -61,7 +143,7 @@
LIMIT 21
'
---
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.11
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.13
'
SELECT "posthog_scheduledchange"."id",
"posthog_scheduledchange"."record_id",
@@ -79,7 +161,43 @@
LIMIT 21
'
---
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.12
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.14
+ '
+ SELECT "posthog_scheduledchange"."id",
+ "posthog_scheduledchange"."record_id",
+ "posthog_scheduledchange"."model_name",
+ "posthog_scheduledchange"."payload",
+ "posthog_scheduledchange"."scheduled_at",
+ "posthog_scheduledchange"."executed_at",
+ "posthog_scheduledchange"."failure_reason",
+ "posthog_scheduledchange"."team_id",
+ "posthog_scheduledchange"."created_at",
+ "posthog_scheduledchange"."created_by_id",
+ "posthog_scheduledchange"."updated_at"
+ FROM "posthog_scheduledchange"
+ WHERE "posthog_scheduledchange"."id" = 2
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.15
+ '
+ SELECT "posthog_scheduledchange"."id",
+ "posthog_scheduledchange"."record_id",
+ "posthog_scheduledchange"."model_name",
+ "posthog_scheduledchange"."payload",
+ "posthog_scheduledchange"."scheduled_at",
+ "posthog_scheduledchange"."executed_at",
+ "posthog_scheduledchange"."failure_reason",
+ "posthog_scheduledchange"."team_id",
+ "posthog_scheduledchange"."created_at",
+ "posthog_scheduledchange"."created_by_id",
+ "posthog_scheduledchange"."updated_at"
+ FROM "posthog_scheduledchange"
+ WHERE "posthog_scheduledchange"."id" = 2
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.16
'
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@@ -178,6 +296,88 @@
'
---
# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.5
+ '
+ SELECT "posthog_organization"."id",
+ "posthog_organization"."name",
+ "posthog_organization"."slug",
+ "posthog_organization"."created_at",
+ "posthog_organization"."updated_at",
+ "posthog_organization"."plugins_access_level",
+ "posthog_organization"."for_internal_metrics",
+ "posthog_organization"."is_member_join_email_enabled",
+ "posthog_organization"."enforce_2fa",
+ "posthog_organization"."customer_id",
+ "posthog_organization"."available_product_features",
+ "posthog_organization"."usage",
+ "posthog_organization"."never_drop_data",
+ "posthog_organization"."setup_section_2_completed",
+ "posthog_organization"."personalization",
+ "posthog_organization"."domain_whitelist",
+ "posthog_organization"."available_features"
+ FROM "posthog_organization"
+ WHERE "posthog_organization"."id" = '00000000-0000-0000-0000-000000000000'::uuid
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.6
+ '
+ SELECT "posthog_team"."id",
+ "posthog_team"."uuid",
+ "posthog_team"."organization_id",
+ "posthog_team"."api_token",
+ "posthog_team"."app_urls",
+ "posthog_team"."name",
+ "posthog_team"."slack_incoming_webhook",
+ "posthog_team"."created_at",
+ "posthog_team"."updated_at",
+ "posthog_team"."anonymize_ips",
+ "posthog_team"."completed_snippet_onboarding",
+ "posthog_team"."has_completed_onboarding_for",
+ "posthog_team"."ingested_event",
+ "posthog_team"."autocapture_opt_out",
+ "posthog_team"."autocapture_exceptions_opt_in",
+ "posthog_team"."autocapture_exceptions_errors_to_ignore",
+ "posthog_team"."session_recording_opt_in",
+ "posthog_team"."session_recording_sample_rate",
+ "posthog_team"."session_recording_minimum_duration_milliseconds",
+ "posthog_team"."session_recording_linked_flag",
+ "posthog_team"."session_recording_network_payload_capture_config",
+ "posthog_team"."capture_console_log_opt_in",
+ "posthog_team"."capture_performance_opt_in",
+ "posthog_team"."surveys_opt_in",
+ "posthog_team"."session_recording_version",
+ "posthog_team"."signup_token",
+ "posthog_team"."is_demo",
+ "posthog_team"."access_control",
+ "posthog_team"."week_start_day",
+ "posthog_team"."inject_web_apps",
+ "posthog_team"."test_account_filters",
+ "posthog_team"."test_account_filters_default_checked",
+ "posthog_team"."path_cleaning_filters",
+ "posthog_team"."timezone",
+ "posthog_team"."data_attributes",
+ "posthog_team"."person_display_name_properties",
+ "posthog_team"."live_events_columns",
+ "posthog_team"."recording_domains",
+ "posthog_team"."primary_dashboard_id",
+ "posthog_team"."extra_settings",
+ "posthog_team"."correlation_config",
+ "posthog_team"."session_recording_retention_period_days",
+ "posthog_team"."plugins_opt_in",
+ "posthog_team"."opt_out_capture",
+ "posthog_team"."event_names",
+ "posthog_team"."event_names_with_usage",
+ "posthog_team"."event_properties",
+ "posthog_team"."event_properties_with_usage",
+ "posthog_team"."event_properties_numerical",
+ "posthog_team"."external_data_workspace_id",
+ "posthog_team"."external_data_workspace_last_synced_at"
+ FROM "posthog_team"
+ WHERE "posthog_team"."id" = 2
+ LIMIT 21
+ '
+---
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.7
'
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@@ -199,7 +399,7 @@
LIMIT 21
'
---
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.6
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.8
'
SELECT "posthog_user"."id",
"posthog_user"."password",
@@ -230,7 +430,7 @@
LIMIT 21
'
---
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.7
+# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.9
'
SELECT "posthog_featureflag"."id",
"posthog_featureflag"."key",
@@ -253,39 +453,3 @@
AND "posthog_featureflag"."team_id" = 2)
'
---
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.8
- '
- SELECT "posthog_scheduledchange"."id",
- "posthog_scheduledchange"."record_id",
- "posthog_scheduledchange"."model_name",
- "posthog_scheduledchange"."payload",
- "posthog_scheduledchange"."scheduled_at",
- "posthog_scheduledchange"."executed_at",
- "posthog_scheduledchange"."failure_reason",
- "posthog_scheduledchange"."team_id",
- "posthog_scheduledchange"."created_at",
- "posthog_scheduledchange"."created_by_id",
- "posthog_scheduledchange"."updated_at"
- FROM "posthog_scheduledchange"
- WHERE "posthog_scheduledchange"."id" = 2
- LIMIT 21
- '
----
-# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.9
- '
- SELECT "posthog_scheduledchange"."id",
- "posthog_scheduledchange"."record_id",
- "posthog_scheduledchange"."model_name",
- "posthog_scheduledchange"."payload",
- "posthog_scheduledchange"."scheduled_at",
- "posthog_scheduledchange"."executed_at",
- "posthog_scheduledchange"."failure_reason",
- "posthog_scheduledchange"."team_id",
- "posthog_scheduledchange"."created_at",
- "posthog_scheduledchange"."created_by_id",
- "posthog_scheduledchange"."updated_at"
- FROM "posthog_scheduledchange"
- WHERE "posthog_scheduledchange"."id" = 2
- LIMIT 21
- '
----
diff --git a/posthog/tasks/test/test_process_scheduled_changes.py b/posthog/tasks/test/test_process_scheduled_changes.py
index 866f3847c5d34..0e1fb9b9db3f8 100644
--- a/posthog/tasks/test/test_process_scheduled_changes.py
+++ b/posthog/tasks/test/test_process_scheduled_changes.py
@@ -63,6 +63,57 @@ def test_schedule_feature_flag_add_release_condition(self) -> None:
updated_flag = FeatureFlag.objects.get(key="flag-1")
self.assertEqual(updated_flag.filters["groups"][0], new_release_condition)
+ def test_schedule_feature_flag_add_release_condition_preserve_variants(self) -> None:
+ variants = [
+ {
+ "key": "first-variant",
+ "name": "First Variant",
+ "rollout_percentage": 25,
+ },
+ {
+ "key": "second-variant",
+ "name": "Second Variant",
+ "rollout_percentage": 75,
+ },
+ ]
+
+ feature_flag = FeatureFlag.objects.create(
+ name="Flag 1",
+ key="flag-1",
+ active=False,
+ team=self.team,
+ created_by=self.user,
+ filters={
+ "groups": [],
+ "multivariate": {"variants": variants},
+ },
+ )
+
+ new_release_condition = {
+ "variant": None,
+ "properties": [{"key": "$browser", "type": "person", "value": ["Chrome"], "operator": "exact"}],
+ "rollout_percentage": 30,
+ }
+
+ payload = {
+ "operation": "add_release_condition",
+ "value": {"groups": [new_release_condition], "payloads": {}, "multivariate": None},
+ }
+
+ ScheduledChange.objects.create(
+ team=self.team,
+ record_id=feature_flag.id,
+ model_name="FeatureFlag",
+ payload=payload,
+ scheduled_at=(datetime.now(timezone.utc) - timedelta(seconds=30)),
+ )
+
+ process_scheduled_changes()
+
+ updated_flag = FeatureFlag.objects.get(key="flag-1")
+ self.assertEqual(updated_flag.filters["groups"][0], new_release_condition)
+ self.assertEqual(updated_flag.filters["multivariate"]["variants"], variants)
+
def test_schedule_feature_flag_invalid_payload(self) -> None:
feature_flag = FeatureFlag.objects.create(
name="Flag 1",