Skip to content

Commit 4457ca0

Browse files
feat: Better handle multivariate flags on sync_feature_flags
We now create the feature flags from `contants.tsx` respecting the multivariate flags and also accepting options (for example what multivariate flags we should use)
1 parent 43542c4 commit 4457ca0

File tree

2 files changed

+31
-29
lines changed

2 files changed

+31
-29
lines changed

frontend/src/lib/constants.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ export const WEBHOOK_SERVICES: Record<string, string> = {
148148

149149
// NOTE: Run `dev:sync-flags` locally to sync these flags into your local project
150150
// or if you're running flox + mprocs you can also run the `sync-feature-flags` process
151+
//
152+
// If this is a multivariate flag, please add the `multivariate=true` tag at the end of your comment
153+
// if you want the script to properly create a multivariate flag. You can also specify the different
154+
// variant keys separated by commas, e.g. `multivariate=control,test,something_else`
151155
export const FEATURE_FLAGS = {
152156
// Eternal feature flags, shouldn't be removed, helpful for debugging/maintenance reasons
153157
BILLING_FORECASTING_ISSUES: 'billing-forecasting-issues', // owner: #team-billing, see `Billing.tsx`, used to raise a warning when billing is having problems
@@ -296,10 +300,10 @@ export const FEATURE_FLAGS = {
296300
REMOTE_CONFIG: 'remote-config', // owner: #team-platform-features
297301
REPLAY_DISCARD_RAW_SNAPSHOTS: 'replay-discard-raw-snapshots', // owner: @pauldambra #team-replay
298302
REPLAY_FILTERS_REDESIGN: 'replay-filters-redesign', // owner: @ksvat #team-replay
299-
REPLAY_NEW_DETECTED_URL_COLLECTIONS: 'replay-new-detected-url-collections', // owner: @ksvat #team-replay multivariate
303+
REPLAY_NEW_DETECTED_URL_COLLECTIONS: 'replay-new-detected-url-collections', // owner: @ksvat #team-replay multivariate=true
300304
REPLAY_WAIT_FOR_FULL_SNAPSHOT_PLAYBACK: 'replay-wait-for-full-snapshot-playback', // owner: @ksvat #team-replay
301305
REPLAY_X_LLM_ANALYTICS_CONVERSATION_VIEW: 'replay-x-llm-analytics-conversation-view', // owner: @pauldambra #team-replay
302-
REPLAY_YIELDING_PROCESSING: 'replay-yielding-processing', // owner: @pauldambra #team-replay multivariate: worker, yielding, worker_and_yielding
306+
REPLAY_YIELDING_PROCESSING: 'replay-yielding-processing', // owner: @pauldambra #team-replay multivariate=worker,yielding,worker_and_yielding
303307
SCHEDULE_FEATURE_FLAG_VARIANTS_UPDATE: 'schedule-feature-flag-variants-update', // owner: @gustavo #team-feature-flags
304308
SCHEMA_MANAGEMENT: 'schema-management', // owner: @aspicer
305309
SEEKBAR_PREVIEW_SCRUBBING: 'seekbar-preview-scrubbing', // owner: @pauldambra #team-replay
@@ -311,7 +315,7 @@ export const FEATURE_FLAGS = {
311315
SURVEYS_EXPERIMENTS_CROSS_SELL: 'surveys-experiments-cross-sell', // owner: @adboio #team-surveys
312316
SURVEYS_FF_CROSS_SELL: 'surveys-ff-cross-sell', // owner: @adboio #team-surveys
313317
SURVEYS_FUNNELS_CROSS_SELL: 'survey-funnels-cross-sell', // owner: @adboio #team-surveys
314-
SURVEYS_INSIGHT_BUTTON_EXPERIMENT: 'ask-users-why-ai-vs-quickcreate', // owner: @adboio #team-surveys multivariate
318+
SURVEYS_INSIGHT_BUTTON_EXPERIMENT: 'ask-users-why-ai-vs-quickcreate', // owner: @adboio #team-surveys multivariate=true
315319
SWITCH_SUBSCRIPTION_PLAN: 'switch-subscription-plan', // owner: @a-lider #team-platform-features
316320
TASK_SUMMARIES: 'task-summaries', // owner: #team-llm-analytics
317321
TASKS: 'tasks', // owner: #team-llm-analytics

posthog/management/commands/sync_feature_flags.py

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# ruff: noqa: T201 allow print statements
22

3+
import re
34
from typing import cast
45

56
from django.core.management.base import BaseCommand
67

7-
from posthog.models import FeatureFlag, Project, User
8+
from posthog.models import FeatureFlag, Team, User
89

910
# These flags won't be enabled when syncing feature flags
1011
# Turn these on for flags that heavily change the behavior and that you wouldn't like
@@ -36,38 +37,38 @@ def handle(self, *args, **options):
3637
else:
3738
try:
3839
flag = line.split("'")[1]
39-
if flag.endswith("_EXPERIMENT") or "multivariate" in line:
40-
flags[flag] = "multivariate"
40+
multivariate_match = re.search(r"multivariate=([^\s/]+)", line)
41+
if multivariate_match:
42+
variant_keys = [key.strip() for key in multivariate_match.group(1).split(",")]
43+
if len(variant_keys) == 1 and variant_keys[0] == "true":
44+
flags[flag] = ["control", "test"]
45+
else:
46+
flags[flag] = variant_keys
4147
else:
4248
flags[flag] = "boolean"
4349
except IndexError:
4450
pass
45-
4651
elif "export const FEATURE_FLAGS" in line:
4752
parsing_flags = True
4853

4954
first_user = cast(User, User.objects.first())
50-
for project in Project.objects.all():
51-
existing_flags = FeatureFlag.objects.filter(team__project_id=project.id).values_list("key", flat=True)
52-
deleted_flags = FeatureFlag.objects.filter(team__project_id=project.id, deleted=True).values_list(
53-
"key", flat=True
54-
)
55+
for team in Team.objects.all():
56+
existing_flags = FeatureFlag.objects.filter(team=team).values_list("key", flat=True)
57+
deleted_flags = FeatureFlag.objects.filter(team=team, deleted=True).values_list("key", flat=True)
5558
for flag in flags.keys():
5659
flag_type = flags[flag]
5760
is_enabled = flag not in INACTIVE_FLAGS
5861

5962
if flag in deleted_flags:
60-
ff = FeatureFlag.objects.filter(team__project_id=project.id, key=flag)[0]
63+
ff = FeatureFlag.objects.filter(team=team, key=flag)[0]
6164
ff.deleted = False
6265
ff.active = is_enabled
6366
ff.save()
64-
print(
65-
f"Undeleted feature flag '{flag} for project {project.id} {' - ' + project.name if project.name else ''}"
66-
)
67+
print(f"Undeleted feature flag '{flag} for team {team.id} {' - ' + team.name if team.name else ''}")
6768
elif flag not in existing_flags:
68-
if flag_type == "multivariate":
69+
if isinstance(flag_type, list):
6970
FeatureFlag.objects.create(
70-
team=project.teams.first(),
71+
team=team,
7172
rollout_percentage=100,
7273
name=flag,
7374
key=flag,
@@ -78,29 +79,26 @@ def handle(self, *args, **options):
7879
"multivariate": {
7980
"variants": [
8081
{
81-
"key": "control",
82-
"name": "Control",
83-
"rollout_percentage": 0,
84-
},
85-
{
86-
"key": "test",
87-
"name": "Test",
88-
"rollout_percentage": 100,
89-
},
82+
"key": key,
83+
"name": key.capitalize(),
84+
"rollout_percentage": 100 if index == len(flag_type) - 1 else 0,
85+
}
86+
for index, key in enumerate(flag_type)
9087
]
9188
},
9289
},
9390
)
9491
else:
9592
FeatureFlag.objects.create(
96-
team=project.teams.first(),
93+
team=team,
9794
rollout_percentage=100,
9895
name=flag,
9996
key=flag,
10097
created_by=first_user,
10198
active=is_enabled,
10299
filters={"groups": [{"properties": [], "rollout_percentage": 100}], "payloads": {}},
103100
)
101+
104102
print(
105-
f"Created feature flag '{flag} for project {project.id} {' - ' + project.name if project.name else ''}"
103+
f"Created feature flag '{flag} for team {team.id} {' - ' + team.name if team.name else ''}{f' (multivariate: {', '.join(flag_type)})' if isinstance(flag_type, list) else ''}"
106104
)

0 commit comments

Comments
 (0)