-
Notifications
You must be signed in to change notification settings - Fork 25
Description
Summary
When using local evaluation with traits in the TraitConfig format (e.g. { value: 'gold', transient: true }), segment conditions always fail to match. This causes segment overrides to not be applied, returning default flag values instead.
Remote evaluation handles this correctly. Only local evaluation is affected.
- Affects
getIdentityFlagsandgetIdentitySegments - Affects SDK versions 7.x and 8.x
How to Reproduce
Setup
- A feature flag (e.g.
my-feature) with default value"default" - A segment with rule: trait
tierExactly Matchesgold - A segment override on that feature with value
"gold override"
Code
const flagsmith = new Flagsmith({
environmentKey: 'your-server-key',
enableLocalEvaluation: true,
});
// ✅ Plain traits — works correctly
const flags1 = await flagsmith.getIdentityFlags('user-1', {
tier: 'gold'
});
console.log(flags1.getFeatureValue('my-feature')); // "gold override" ✅
// ❌ TraitConfig format — bug: segment override not applied
const flags2 = await flagsmith.getIdentityFlags('user-1', {
tier: { value: 'gold', transient: true }
});
console.log(flags2.getFeatureValue('my-feature')); // "default" ❌ (should be "gold override")Root Cause
The SDK has two evaluation paths:
getIdentityFlags(identifier, traits)
│
├─ Remote eval → generateIdentitiesData() → isTraitConfig() unwraps value ✅
│ (sdk/utils.ts)
│
└─ Local eval → getIdentityFlagsFromDocument() → passes raw object ❌
(sdk/index.ts)
Remote evaluation (correct)
In sdk/utils.ts, generateIdentitiesData correctly detects and unwraps TraitConfig:
if (isTraitConfig(value)) {
return {
trait_key: key,
trait_value: value.value, // extracts 'gold' ✅
transient: value.transient
};
}Local evaluation (broken)
In sdk/index.ts, both getIdentityFlagsFromDocument and getIdentitySegments pass traits without unwrapping:
Object.keys(traits).map(key => ({
key,
value: traits[key] // passes { value: 'gold', transient: true } as-is ❌
}))This raw object ends up in TraitModel.traitValue. When the engine evaluates the segment condition:
Segment condition: "tier EQUAL gold"
Trait value stored: { value: 'gold', transient: true } ← an object, not a string
Comparison: 'gold' == { value: 'gold', transient: true } → false (always)
The comparison always fails → segment never matches → default flag value returned.
Validated Against Real Staging Environment
This bug was reproduced and validated end-to-end against a real Flagsmith staging environment:
| SDK State | Plain traits result | TraitConfig result | Status |
|---|---|---|---|
| Fix reverted | "segment override" |
"default" (wrong — fell through to lower-priority segment) |
MISMATCH — bug confirmed |
| Fix applied | "segment override" |
"segment override" |
MATCH — bug fixed |
The staging environment had:
- A segment with rule: a trait Exactly Matches a specific value
- A segment override on a feature with a distinct override value
- The segment set to highest priority
With the fix reverted, the TraitConfig format failed to match the segment, falling through to a lower-priority segment. With the fix applied, both formats correctly matched.
Fix
The fix reuses the existing isTraitConfig helper in both local evaluation methods:
Object.keys(traits).map(key => ({
key,
- value: traits[key]
+ value: isTraitConfig(traits[key]) ? traits[key].value : traits[key]
}))Applied in two places:
getIdentityFlagsFromDocument— forgetIdentityFlagswith local evaluationgetIdentitySegments— for segment listing with local evaluation