-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(experiments): Create experiment with existing feature flag #28004
Conversation
Size Change: +330 B (+0.03%) Total Size: 1.17 MB ℹ️ View Unchanged
|
@@ -252,6 +253,23 @@ def validate_parameters(self, value): | |||
|
|||
return value | |||
|
|||
def validate_existing_feature_flag_for_experiment(self, feature_flag: FeatureFlag): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel reasonably confident in this validation logic but would love a second pair of eyes on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good to me 👍
<div className="my-10"> | ||
<LemonBanner type="info"> | ||
Existing feature flag configuration will be applied to the experiment. | ||
</LemonBanner> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Variants
is the most appropriate header to have before this banner.
actions.setExperimentManualErrors({ | ||
...existingErrors, | ||
feature_flag_key: validateFeatureFlagKey(featureFlagKey), | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem here: when a new field is focused, the manual errors state is cleared and the error state is reset to the previous value of experimentErrors. As such, it's not possible to assign the value to a reducer and then reference the reducer in errors: ({
because the reducer will contain the value that existed before validateFeatureFlag()
fired.
Workaround ideas?
Branch
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One problem here: when a new field is focused, the manual errors state is cleared and the error state is reset to the previous value of experimentErrors.
It actually happens onBlur
for the field:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
This PR adds functionality to create experiments using existing feature flags, with robust validation and UI improvements. Here's a concise summary of the key changes:
- Added feature flag selection modal in
ExperimentForm.tsx
with validation tooltips and error states - Implemented
featureFlagEligibleForExperiment
validation requiring multiple variants with 'control' first - Added async feature flag validation in
experimentLogic.tsx
with proper state management - Modified backend validation in
experiments.py
to support and validate existing feature flag usage - Added analytics tracking for feature flag modal interactions and selections
Key implementation points:
- Feature flags must have multiple variants with 'control' as first variant
- Flags cannot be deleted or associated with other experiments
- UI provides clear feedback through tooltips and field errors
- Backend validation ensures data integrity
- Analytics track feature flag selection workflow
The changes look well-structured with good error handling and user feedback throughout the feature flag selection process.
7 file(s) reviewed, 9 comment(s)
Edit PR Review Bot Settings | Greptile
|
||
variants = feature_flag.filters.get("multivariate", {}).get("variants", []) | ||
|
||
if len(variants) and len(variants) > 1: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The condition len(variants) and len(variants) > 1
can be simplified to just len(variants) > 1
since that covers both cases
if len(variants) and len(variants) > 1: | |
if len(variants) > 1: |
let disabledReason: string | undefined = undefined | ||
try { | ||
featureFlagEligibleForExperiment(flag) | ||
} catch (error) { | ||
disabledReason = (error as Error).message | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: error is implicitly typed as unknown here - should explicitly catch Error type
let disabledReason: string | undefined = undefined | |
try { | |
featureFlagEligibleForExperiment(flag) | |
} catch (error) { | |
disabledReason = (error as Error).message | |
} | |
let disabledReason: string | undefined = undefined | |
try { | |
featureFlagEligibleForExperiment(flag) | |
} catch (error: unknown) { | |
disabledReason = (error instanceof Error) ? error.message : String(error) | |
} |
to={urls.featureFlag(flag.id as number)} | ||
target="_blank" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: unsafe type assertion with 'as number' - should validate id exists first
const featureFlag = { ...baseFeatureFlag, is_remote_configuration: true } | ||
expect(() => featureFlagEligibleForExperiment(featureFlag)).toThrow( | ||
'Feature flag must use multiple variants with control as the first variant.' | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Error message doesn't match the actual issue. Should be about remote configuration not being supported rather than about variants.
const featureFlag = { ...baseFeatureFlag, is_remote_configuration: true } | |
expect(() => featureFlagEligibleForExperiment(featureFlag)).toThrow( | |
'Feature flag must use multiple variants with control as the first variant.' | |
) | |
const featureFlag = { ...baseFeatureFlag, is_remote_configuration: true } | |
expect(() => featureFlagEligibleForExperiment(featureFlag)).toThrow( | |
'Remote configuration feature flags cannot be used in experiments.' | |
) |
let isValid | ||
try { | ||
isValid = featureFlagEligibleForExperiment(matchingFlag) | ||
} catch (error) { | ||
isValid = false | ||
} | ||
actions.setIsExistingFeatureFlag(true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: The try-catch block swallows the error details. Consider logging the error or handling specific error cases differently.
export function featureFlagEligibleForExperiment(featureFlag: FeatureFlagType): true { | ||
if (featureFlag.deleted) { | ||
throw new Error('Feature flag must not be deleted.') | ||
} | ||
|
||
if (featureFlag.experiment_set && featureFlag.experiment_set.length > 0) { | ||
throw new Error('Feature flag is already associated with an experiment.') | ||
} | ||
|
||
if (featureFlag.filters.multivariate?.variants?.length && featureFlag.filters.multivariate.variants.length > 1) { | ||
if (featureFlag.filters.multivariate.variants[0].key !== 'control') { | ||
throw new Error('Feature flag must have control as the first variant.') | ||
} | ||
return true | ||
} | ||
|
||
throw new Error('Feature flag must use multiple variants with control as the first variant.') | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The function returns true
but never returns false
- it throws errors instead. Consider either changing return type to void
or making it return a boolean.
throw new Error('Feature flag is already associated with an experiment.') | ||
} | ||
|
||
if (featureFlag.filters.multivariate?.variants?.length && featureFlag.filters.multivariate.variants.length > 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Optional chaining on variants.length could be simplified to variants?.length > 1
since you're already checking for truthiness
if (featureFlag.filters.multivariate?.variants?.length && featureFlag.filters.multivariate.variants.length > 1) { | |
if (featureFlag.filters.multivariate?.variants?.length > 1) { |
@jurajmajerik You probably started reviewing before a302b2a was applied. |
@jurajmajerik Good suggestions, thank you:
Kinda. It's actually mostly stored on the feature flag: posthog/ee/clickhouse/views/experiments.py Lines 304 to 309 in 7f13464
I thought about including it on the form, but I feel like it's more tied to the feature flag configuration conceptually. Another example: Is this something you're strongly opinionated about? |
Yes, but the selector in the form only sets the The |
All good now! |
Per our conversation in Slack, I removed the Holdout selector from the creation form entirely. |
This is a very useful feature, thanks for doing this! |
Previously #27938
Changes
Allows an existing feature flag to be assigned to a new experiment:
The feature flag has to match some criteria:
control
.The modal shows a tooltip if you try to select a feature flag that isn't eligible:
If you manually specify an ineligible feature flag, you'll see an error in the field:
Also removes the Holdout generally, for consistency between UX.
How did you test this code?
Happy path:
control
as the first variant.