Skip to content

TraitConfig values not unwrapped during local evaluation, causing segment overrides to fail #253

@talissoncosta

Description

@talissoncosta

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 getIdentityFlags and getIdentitySegments
  • Affects SDK versions 7.x and 8.x

How to Reproduce

Setup

  1. A feature flag (e.g. my-feature) with default value "default"
  2. A segment with rule: trait tier Exactly Matches gold
  3. 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:

  1. getIdentityFlagsFromDocument — for getIdentityFlags with local evaluation
  2. getIdentitySegments — for segment listing with local evaluation

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions