Skip to content

Commit 45c4594

Browse files
committed
feat: added-reasons-to-flag-object
1 parent f1ee3ef commit 45c4594

5 files changed

Lines changed: 58 additions & 18 deletions

File tree

flagsmith-engine/features/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export enum TARGETING_REASONS {
22
DEFAULT = 'DEFAULT',
3-
TARGETING_MATCH = 'TARGETING_MATCH'
3+
TARGETING_MATCH = 'TARGETING_MATCH',
4+
SPLIT = 'SPLIT'
45
}

flagsmith-engine/index.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,28 +108,34 @@ export function evaluateFeatures(
108108
const segmentOverride = segmentOverrides[feature.feature_key];
109109
const finalFeature = segmentOverride ? segmentOverride.feature : feature;
110110
const hasOverride = !!segmentOverride;
111-
const reason = getTargetingMatchReason(segmentOverride);
111+
112+
const { value: evaluatedValue, reason: evaluatedReason } = hasOverride
113+
? { value: finalFeature.value, reason: undefined }
114+
: evaluateFeatureValue(finalFeature, context.identity?.key);
112115

113116
flags.push({
114117
feature_key: finalFeature.feature_key,
115118
name: finalFeature.name,
116119
enabled: finalFeature.enabled,
117-
value: hasOverride
118-
? finalFeature.value
119-
: evaluateFeatureValue(finalFeature, context.identity?.key),
120-
reason
120+
value: evaluatedValue,
121+
reason:
122+
evaluatedReason ??
123+
getTargetingMatchReason({ type: 'SEGMENT', override: segmentOverride })
121124
});
122125
}
123126

124127
return flags;
125128
}
126129

127-
function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): any {
130+
function evaluateFeatureValue(
131+
feature: FeatureContext,
132+
identityKey?: string
133+
): { value: any; reason?: string } {
128134
if (!!feature.variants && feature.variants.length > 0 && !!identityKey) {
129135
return getMultivariateFeatureValue(feature, identityKey);
130136
}
131137

132-
return feature.value;
138+
return { value: feature.value, reason: undefined };
133139
}
134140

135141
/**
@@ -142,19 +148,25 @@ function evaluateFeatureValue(feature: FeatureContext, identityKey?: string): an
142148
* @param identityKey - The identity key used for deterministic variant selection
143149
* @returns The variant value if the identity falls within a variant's range, otherwise the default feature value
144150
*/
145-
function getMultivariateFeatureValue(feature: FeatureContext, identityKey?: string): any {
151+
function getMultivariateFeatureValue(
152+
feature: FeatureContext,
153+
identityKey?: string
154+
): { value: any; reason?: string } {
146155
const percentageValue = getHashedPercentageForObjIds([feature.key, identityKey]);
147156

148157
let startPercentage = 0;
149158
for (const variant of feature?.variants || []) {
150159
const limit = startPercentage + variant.weight;
151160

152161
if (startPercentage <= percentageValue && percentageValue < limit) {
153-
return variant.value;
162+
return {
163+
value: variant.value,
164+
reason: getTargetingMatchReason({ type: 'SPLIT', weight: variant.weight })
165+
};
154166
}
155167
startPercentage = limit;
156168
}
157-
return feature.value;
169+
return { value: feature.value, reason: undefined };
158170
}
159171

160172
export function shouldApplyOverride(
@@ -174,8 +186,28 @@ export function isHigherPriority(
174186
return (priorityA ?? Infinity) < (priorityB ?? Infinity);
175187
}
176188

177-
const getTargetingMatchReason = (segmentOverride: SegmentOverride) => {
178-
return segmentOverride
179-
? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${segmentOverride.segmentName}`
180-
: TARGETING_REASONS.DEFAULT;
189+
export type TargetingMatchReason =
190+
| {
191+
type: 'SEGMENT';
192+
override: SegmentOverride;
193+
}
194+
| {
195+
type: 'SPLIT';
196+
weight: number;
197+
};
198+
199+
const getTargetingMatchReason = (matchObject: TargetingMatchReason) => {
200+
const { type } = matchObject;
201+
202+
if (type === 'SEGMENT') {
203+
return matchObject.override
204+
? `${TARGETING_REASONS.TARGETING_MATCH}; segment=${matchObject.override.segmentName}`
205+
: TARGETING_REASONS.DEFAULT;
206+
}
207+
208+
if (type === 'SPLIT') {
209+
return `${TARGETING_REASONS.SPLIT}; weight=${matchObject.weight}`;
210+
}
211+
212+
return TARGETING_REASONS.DEFAULT;
181213
};

sdk/models.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export class Flag extends BaseFlag {
5050
* The programmatic name for this feature, unique per Flagsmith project.
5151
*/
5252
featureName: string;
53+
/**
54+
* The reason for this feature, unique per Flagsmith project.
55+
*/
56+
reason?: string;
5357

5458
constructor(params: {
5559
value: FlagValue;
@@ -62,6 +66,7 @@ export class Flag extends BaseFlag {
6266
super(params.value, params.enabled, !!params.isDefault);
6367
this.featureId = params.featureId;
6468
this.featureName = params.featureName;
69+
this.reason = params.reason;
6570
}
6671

6772
static fromFeatureStateModel(
@@ -81,7 +86,8 @@ export class Flag extends BaseFlag {
8186
enabled: flagResult.enabled,
8287
value: flagResult.value ?? null,
8388
featureId: Number(flagResult.feature_key),
84-
featureName: flagResult.name
89+
featureName: flagResult.name,
90+
reason: flagResult.reason
8591
});
8692
}
8793

@@ -90,7 +96,8 @@ export class Flag extends BaseFlag {
9096
enabled: flagData['enabled'],
9197
value: flagData['feature_state_value'] ?? flagData['value'],
9298
featureId: flagData['feature']['id'],
93-
featureName: flagData['feature']['name']
99+
featureName: flagData['feature']['name'],
100+
reason: flagData['feature']['reason']
94101
});
95102
}
96103
}

tests/engine/e2e/engine.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ describe('Engine Integration Tests', () => {
5757
for (let i = 0; i < sortedEngineFlags.length; i++) {
5858
expect(sortedEngineFlags[i].value).toBe(sortedAPIFlags[i].value);
5959
expect(sortedEngineFlags[i].enabled).toBe(sortedAPIFlags[i].enabled);
60+
expect(sortedEngineFlags[i].reason).toBe(sortedAPIFlags[i].reason);
6061
}
6162
});
6263
});

tests/engine/unit/segments/segments_model.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,6 @@ const conditionMatchCases: [string, string | number | boolean | null, string, bo
107107
test('test_segment_condition_matches_trait_value', () => {
108108
for (const testCase of conditionMatchCases) {
109109
const [operator, traitValue, conditionValue, expectedResult] = testCase;
110-
console.log(operator, traitValue, conditionValue, expectedResult);
111110
expect(
112111
new SegmentConditionModel(operator, conditionValue, 'foo').matchesTraitValue(traitValue)
113112
).toBe(expectedResult);

0 commit comments

Comments
 (0)