Skip to content

Commit 7e191b8

Browse files
authored
fix: Missing variants in feature engine context (#257)
1 parent c18c3b3 commit 7e191b8

File tree

2 files changed

+106
-12
lines changed

2 files changed

+106
-12
lines changed

flagsmith-engine/evaluation/evaluationContext/mappers.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { EnvironmentModel } from '../../environments/models.js';
1313
import { IdentityModel } from '../../identities/models.js';
1414
import { TraitModel } from '../../identities/traits/models.js';
15+
import { FeatureStateModel } from '../../features/models.js';
1516
import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../segments/constants.js';
1617
import { createHash } from 'node:crypto';
1718
import { uuidToBigInt } from '../../features/util.js';
@@ -48,21 +49,12 @@ function mapEnvironmentModelToEvaluationContext(
4849

4950
const features: FeaturesWithMetadata<SDKFeatureMetadata> = {};
5051
for (const fs of environment.featureStates) {
51-
const variants =
52-
fs.multivariateFeatureStateValues?.length > 0
53-
? fs.multivariateFeatureStateValues.map(mv => ({
54-
value: mv.multivariateFeatureOption.value,
55-
weight: mv.percentageAllocation,
56-
priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
57-
}))
58-
: undefined;
59-
6052
features[fs.feature.name] = {
6153
key: fs.djangoID?.toString() || fs.featurestateUUID,
6254
name: fs.feature.name,
6355
enabled: fs.enabled,
6456
value: fs.getValue(),
65-
variants,
57+
variants: mapFeatureStateVariants(fs),
6658
priority: fs.featureSegment?.priority,
6759
metadata: {
6860
id: fs.feature.id
@@ -83,6 +75,7 @@ function mapEnvironmentModelToEvaluationContext(
8375
name: fs.feature.name,
8476
enabled: fs.enabled,
8577
value: fs.getValue(),
78+
variants: mapFeatureStateVariants(fs),
8679
priority: fs.featureSegment?.priority,
8780
metadata: {
8881
id: fs.feature.id
@@ -130,6 +123,16 @@ function mapIdentityModelToIdentityContext(
130123
return identityContext;
131124
}
132125

126+
function mapFeatureStateVariants(fs: FeatureStateModel) {
127+
return fs.multivariateFeatureStateValues?.length > 0
128+
? fs.multivariateFeatureStateValues.map(mv => ({
129+
value: mv.multivariateFeatureOption.value,
130+
weight: mv.percentageAllocation,
131+
priority: mv.id ?? uuidToBigInt(mv.mvFsValueUuid)
132+
}))
133+
: undefined;
134+
}
135+
133136
function mapSegmentRuleModelToRule(rule: any): any {
134137
return {
135138
type: rule.type,
@@ -160,6 +163,7 @@ function mapIdentityOverridesToSegments(
160163
name: fs.feature.name,
161164
enabled: fs.enabled,
162165
value: fs.getValue(),
166+
variants: mapFeatureStateVariants(fs),
163167
priority: -Infinity,
164168
metadata: {
165169
id: fs.feature.id

tests/engine/unit/engine.test.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,29 @@ import {
77
shouldApplyOverride
88
} from '../../../flagsmith-engine/index.js';
99
import { CONSTANTS } from '../../../flagsmith-engine/features/constants.js';
10-
import { FeatureModel, FeatureStateModel } from '../../../flagsmith-engine/features/models.js';
10+
import {
11+
FeatureModel,
12+
FeatureSegment,
13+
FeatureStateModel,
14+
MultivariateFeatureOptionModel,
15+
MultivariateFeatureStateValueModel
16+
} from '../../../flagsmith-engine/features/models.js';
1117
import { TraitModel } from '../../../flagsmith-engine/identities/traits/models.js';
1218
import {
1319
environment,
1420
environmentWithSegmentOverride,
1521
feature1,
1622
identity,
1723
identityInSegment,
24+
segment,
1825
segmentConditionProperty,
19-
segmentConditionStringValue
26+
segmentConditionStringValue,
27+
traitMatchingSegment
2028
} from './utils.js';
2129
import { getEvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/mappers.js';
2230
import { TARGETING_REASONS } from '../../../flagsmith-engine/features/types.js';
2331
import { EvaluationContext } from '../../../flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.js';
32+
import { EvaluationContextWithMetadata } from '../../../flagsmith-engine/evaluation/models.js';
2433
import { IDENTITY_OVERRIDE_SEGMENT_NAME } from '../../../flagsmith-engine/segments/constants.js';
2534

2635
test('test_get_evaluation_result_without_any_override', () => {
@@ -356,3 +365,84 @@ test('evaluateFeatures with multivariate evaluation', () => {
356365
const flags = evaluateFeatures(context as any, {});
357366
expect(flags['Multivariate Feature'].value).toBe('variant_b');
358367
});
368+
369+
test('local evaluation returns correct multivariate value for segment override with 100% weight', () => {
370+
// Given
371+
// a feature with two multivariate variants where the segment override
372+
// assigns 100% weight to the second variant
373+
const env = environment();
374+
const seg = segment();
375+
376+
const mvFeature = new FeatureModel(10, 'mv_feature', CONSTANTS.STANDARD);
377+
378+
const controlOption = new MultivariateFeatureOptionModel('control', 1);
379+
const variantOption = new MultivariateFeatureOptionModel('variant_b', 2);
380+
381+
const envFs = new FeatureStateModel(mvFeature, true, 10, 'default');
382+
envFs.multivariateFeatureStateValues = [
383+
new MultivariateFeatureStateValueModel(controlOption, 0, 1),
384+
new MultivariateFeatureStateValueModel(variantOption, 100, 2)
385+
];
386+
env.featureStates.push(envFs);
387+
388+
const overrideFs = new FeatureStateModel(mvFeature, true, 11, 'default');
389+
overrideFs.featureSegment = new FeatureSegment(0);
390+
overrideFs.multivariateFeatureStateValues = [
391+
new MultivariateFeatureStateValueModel(controlOption, 0, 1),
392+
new MultivariateFeatureStateValueModel(variantOption, 100, 2)
393+
];
394+
seg.featureStates.push(overrideFs);
395+
env.project.segments = [seg];
396+
397+
// When
398+
// evaluating flags for an identity that matches the segment
399+
const context = getEvaluationContext(env, identityInSegment(), [traitMatchingSegment()]);
400+
const result = getEvaluationResult(context as EvaluationContextWithMetadata);
401+
const flag = result.flags['mv_feature'];
402+
403+
// Then
404+
// the flag value should be the 100%-weighted variant, not the base default
405+
expect(flag).toBeDefined();
406+
expect(flag.value).toBe('variant_b');
407+
});
408+
409+
test('getEvaluationContext maps multivariate variants onto segment override feature states', () => {
410+
// Given
411+
// a segment override feature state with multivariate values
412+
const env = environment();
413+
const seg = segment();
414+
415+
const mvFeature = new FeatureModel(10, 'mv_feature', CONSTANTS.STANDARD);
416+
env.featureStates.push(new FeatureStateModel(mvFeature, true, 10, 'default'));
417+
418+
const overrideFs = new FeatureStateModel(mvFeature, true, 11, 'default');
419+
overrideFs.featureSegment = new FeatureSegment(0);
420+
overrideFs.multivariateFeatureStateValues = [
421+
new MultivariateFeatureStateValueModel(
422+
new MultivariateFeatureOptionModel('variant_value', 1),
423+
100,
424+
1
425+
)
426+
];
427+
seg.featureStates.push(overrideFs);
428+
env.project.segments = [seg];
429+
430+
// When
431+
// mapping the environment model to an evaluation context
432+
const context = getEvaluationContext(env, identityInSegment(), [traitMatchingSegment()]);
433+
434+
// Then
435+
// the segment override should include the variants array
436+
const segmentOverrides = Object.values(context.segments || {});
437+
const segWithOverrides = segmentOverrides.find(
438+
s => s.overrides && s.overrides.some((o: any) => o.name === 'mv_feature')
439+
);
440+
expect(segWithOverrides).toBeDefined();
441+
442+
const mvOverride = segWithOverrides!.overrides!.find((o: any) => o.name === 'mv_feature');
443+
expect(mvOverride).toBeDefined();
444+
expect((mvOverride as any).variants).toBeDefined();
445+
expect((mvOverride as any).variants).toHaveLength(1);
446+
expect((mvOverride as any).variants[0].value).toBe('variant_value');
447+
expect((mvOverride as any).variants[0].weight).toBe(100);
448+
});

0 commit comments

Comments
 (0)