feat: surface multivariate variant key and use it as the exposure value#401
feat: surface multivariate variant key and use it as the exposure value#401Zaimwa9 wants to merge 3 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for multivariate flag variants in the Flagsmith SDK. It updates the core SDK to extract and store the variant property on flags, uses the variant value instead of the flag value for experiment exposure tracking, and updates the React hooks (useFlags, useExperiment) to handle and trigger re-renders on variant changes. Additionally, types and tests are updated to support this new feature. Feedback from the review highlights two main areas of improvement: first, in flagsmith-core.ts, if a flag is missing (null), the current logic logs a misleading warning about a missing variant, so it is recommended to check if the flag exists before checking for its variant; second, in react.tsx, the use of .map() to mutate an object is an anti-pattern and should be refactored to use .forEach() for better clarity and performance.
| if (!flag?.variant) { | ||
| this.log(`Flagsmith: getExperimentFlag called for "${featureName}" which has no variant; experiments require a multivariate flag. No exposure recorded.`); | ||
| return flag; | ||
| } |
There was a problem hiding this comment.
If flag is null (e.g., if the feature name is misspelled or does not exist), !flag?.variant will evaluate to true. This causes the SDK to log a misleading warning stating that the feature has no variant and requires a multivariate flag, which is confusing since the feature itself is completely missing.
Consider checking if flag is null first and returning early without logging this warning.
| if (!flag?.variant) { | |
| this.log(`Flagsmith: getExperimentFlag called for "${featureName}" which has no variant; experiments require a multivariate flag. No exposure recorded.`); | |
| return flag; | |
| } | |
| if (!flag) { | |
| return null; | |
| } | |
| if (!flag.variant) { | |
| this.log("Flagsmith: getExperimentFlag called for \"" + featureName + "\" which has no variant; experiments require a multivariate flag. No exposure recorded."); | |
| return flag; | |
| } |
| flags | ||
| .map((k) => { | ||
| const variant = flagsmith!.getAllFlags()?.[k]?.variant | ||
| res[k] = { | ||
| enabled: flagsmith!.hasFeature(k), | ||
| value: flagsmith!.getValue(k), | ||
| ...(variant != null ? { variant } : {}), | ||
| } | ||
| }) |
There was a problem hiding this comment.
Using .map() to perform side-effects (mutating res) and chaining it with .concat() is an anti-pattern in JavaScript/TypeScript. .map() is intended for pure transformations and creates a new array that is immediately discarded here, leading to unnecessary memory allocation.
Consider refactoring the entire useMemo block to use forEach for clarity and efficiency:
const res = useMemo(() => {
const res: any = {}
flags.forEach((k) => {
const variant = flagsmith!.getAllFlags()?.[k]?.variant
res[k] = {
enabled: flagsmith!.hasFeature(k),
value: flagsmith!.getValue(k),
...(variant != null ? { variant } : {}),
}
})
traits?.forEach((v) => {
res[v] = flagsmith!.getTrait(v)
})
return res
}, [renderRef])There was a problem hiding this comment.
Out of scope for this one as this is legacy
…ant keys as absent
Thanks for submitting a PR! Please check the boxes below:
docs/if required so people know about the feature.Changes
IFlagsmithFeature.variant— selected variant's key,"control"for the control bucket, omitted otherwise.getExperimentFlagrecords the variant as the$flag_exposurevalue; flags without a variant record no exposure.useExperimentre-fires the exposure when the variant changes.How did you test this code?
Unit tests: variant parsing, exposure values, variant-change re-exposure