Skip to content

[sdk/local-evaluation] Duplicate identity override entries silently drop feature overrides #267

Description

@xolott-ark

Problem

When an environment document returned by the Flagsmith API contains more than one entry in identity_overrides sharing the same identifier, the SDK keeps only the last entry for that identifier during local evaluation. Any identity_features defined on the earlier entries are discarded, so the corresponding flags fall back to the environment default instead of the configured override.

For example, given an environment payload like:

{
  "identity_overrides": [
    {
      "identifier": "multi-override-id",
      "identity_features": [
        {
          "feature": { "name": "some_feature" },
          "feature_state_value": "override-from-first-entry",
          "enabled": false
        }
      ]
    },
    {
      "identifier": "multi-override-id",
      "identity_features": [
        {
          "feature": { "name": "mv_feature" },
          "feature_state_value": "override-from-second-entry",
          "enabled": true
        }
      ]
    }
  ]
}

Calling getIdentityFlags('multi-override-id') with local evaluation enabled returns the override for mv_feature only. some_feature resolves to its environment default, as if the first entry never existed.

Impact

Any identifier whose overrides arrive split across multiple identity_overrides entries gets the wrong flag values during server-side local evaluation. The breakage is silent (no warning, no error) and deterministic for a given payload, so affected identifiers stay broken on every request until the environment payload changes shape.

Reproduction

  1. Use an environment whose identity_overrides array contains two entries with the same identifier, each carrying a different feature in identity_features (e.g. entry A overrides some_feature, entry B overrides mv_feature).
  2. Initialise the SDK with enableLocalEvaluation: true and call getIdentityFlags('<that-identifier>').
  3. Observed: only the features from the last entry are applied. some_feature resolves to its environment default instead of "override-from-first-entry".
  4. Expected: both some_feature and mv_feature resolve to their respective override values and enabled states.

Activity

  1. xolott-ark commented on May 8, 2026

    @xolott-ark
    Author

    I created a PR with the fix #268

  2. added theissue type on May 14, 2026
  3. matthewelwell commented on May 19, 2026

    @matthewelwell
    Contributor

    Hi @xolott-ark , thanks for raising this, and for submitting the PR.

    I am unable to reproduce the bug raised, can I ask how you reproduced it? From what I can see, the response from the API groups the actual overrides into the identity_features key within each entry in the identity_overrides. I've included an example API response below showing this behaviour. I've tested this against a locally hosted deployment of Flagsmith (v2.233.0), and production SaaS and got the same result.

    I've also include an example response from the API, and a short test script to validate the behaviour in the nodejs client.

    Example API response

    Obtained from

    curl https://edge.api.flagsmith.com/api/v1/environment-document \
      --header 'X-Environment-Key: ser.myKey' \
       | jq .identity_overrides
    [
      {
        "identifier": "foouser",
        "environment_api_key": "2tGnSzwbSzCdVVPdgNHSTD",
        "created_date": "2026-05-19T10:32:59.539731Z",
        "identity_features": [
          {
            "feature": {
              "id": 208333,
              "name": "feature_1",
              "type": "STANDARD"
            },
            "enabled": true,
            "django_id": null,
            "feature_segment": null,
            "featurestate_uuid": "d18e33ee-6732-416d-8000-0b25f0810879",
            "feature_state_value": null,
            "multivariate_feature_state_values": []
          },
          {
            "feature": {
              "id": 208334,
              "name": "feature_2",
              "type": "STANDARD"
            },
            "enabled": true,
            "django_id": null,
            "feature_segment": null,
            "featurestate_uuid": "61016c14-dcf8-4f76-8867-278ab7386a91",
            "feature_state_value": null,
            "multivariate_feature_state_values": []
          },
          {
            "feature": {
              "id": 208336,
              "name": "feature_4",
              "type": "STANDARD"
            },
            "enabled": true,
            "django_id": null,
            "feature_segment": null,
            "featurestate_uuid": "67c5cee2-1057-4c33-88b8-61c65f4d0cda",
            "feature_state_value": null,
            "multivariate_feature_state_values": []
          }
        ],
        "identity_traits": [],
        "identity_uuid": "79a6bd8c-9e99-4d80-ad78-04d12b98320a",
        "django_id": null,
        "dashboard_alias": null,
        "composite_key": "2tGnSzwbSzCdVVPdgNHSTD_foouser"
      },
      {
        "identifier": "baruser",
        "environment_api_key": "2tGnSzwbSzCdVVPdgNHSTD",
        "created_date": "2026-05-19T10:46:23.862937Z",
        "identity_features": [
          {
            "feature": {
              "id": 208335,
              "name": "feature_3",
              "type": "STANDARD"
            },
            "enabled": true,
            "django_id": null,
            "feature_segment": null,
            "featurestate_uuid": "dcbcc1fa-1df3-43f3-ba78-78cf7789357a",
            "feature_state_value": null,
            "multivariate_feature_state_values": []
          }
        ],
        "identity_traits": [],
        "identity_uuid": "9c4b99eb-de49-4ea7-abcc-49de79724285",
        "django_id": null,
        "dashboard_alias": null,
        "composite_key": "2tGnSzwbSzCdVVPdgNHSTD_baruser"
      }
    ]
    Reproduction script

    Prequisites:

    • flagsmith-nodejs installed
    • a server-side environment key
    • a project with 4 features: feature_1|2|3|4
    • 2 identities: foouser and baruser, with the overrides configured as per above API response snippet
    import { Flagsmith } from 'flagsmith-nodejs';
    
    const environmentKey = 'ser.yourKey';  // add your environment key here
    
    const flagsmithLocal = new Flagsmith({
        environmentKey: environmentKey,
        enableLocalEvaluation: true,
    });
    
    let identifier1 = 'foouser';
    let identifier2 = 'baruser';
    
    var fooUserFlags = await flagsmithLocal.getIdentityFlags(identifier1);
    
    let feature1ValueFooUser = fooUserFlags.isFeatureEnabled('feature_1');
    let feature2ValueFooUser = fooUserFlags.isFeatureEnabled('feature_2');
    let feature3ValueFooUser = fooUserFlags.isFeatureEnabled('feature_3');
    let feature4ValueFooUser = fooUserFlags.isFeatureEnabled('feature_4');
    
    console.log('--- foouser ---');
    console.log(`feature_1: ${feature1ValueFooUser}`);
    console.log(`feature_2: ${feature2ValueFooUser}`);
    console.log(`feature_3: ${feature3ValueFooUser}`);
    console.log(`feature_4: ${feature4ValueFooUser}`);
    console.log('---------------');
    
    var barUserFlags = await flagsmithLocal.getIdentityFlags(identifier2);
    
    let feature1ValueBarUser = barUserFlags.isFeatureEnabled('feature_1');
    let feature2ValueBarUser = barUserFlags.isFeatureEnabled('feature_2');
    let feature3ValueBarUser = barUserFlags.isFeatureEnabled('feature_3');
    let feature4ValueBarUser = barUserFlags.isFeatureEnabled('feature_4');
    
    console.log('--- baruser ---');
    console.log(`feature_1: ${feature1ValueBarUser}`);
    console.log(`feature_2: ${feature2ValueBarUser}`);
    console.log(`feature_3: ${feature3ValueBarUser}`);
    console.log(`feature_4: ${feature4ValueBarUser}`);
    console.log('---------------');
  4. xolott-ark commented on May 19, 2026

    @xolott-ark
    Author

    Thanks for taking a look at this @matthewelwell

    This behaviour is present in our SaaS account, and it broke some production releases we tried a few weeks ago.

    For context, we have close to 200 flags and around 10K identities with close to 9K overrides. We initially thought it was something in our code and the way we use flagsmith, but I followed these steps:

    • Using flagsmith-nodejs 6.2.0 (we can't upgrade beyond 18 due to some internal limitations)
    • I navigated the package (added some console.log for debugging to understand what the package is doing and the data it gets) and found the endpoint used
    • Hit that endpoint manually using Postman with my SDK key and got the full payload
    • Confirmed flagsmith is correctly paginating the endpoint.
    • Confirmed that overrides are not grouped in the API payload (could this be related to the pagination logic in the server)
    • Confirmed that flagsmith wasn't grouping all the overrides for the requested identity.

    The payload shared in the initial post is extracted directly from the API response we get, and it has this shape:

    {
      "identity_overrides": [
        {
          "identifier": "multi-override-id",
          "identity_features": [
            {
              "feature": { "name": "some_feature" },
              "feature_state_value": "override-from-first-entry",
              "enabled": false
            }
          ]
        },
        {
          "identifier": "multi-override-id",
          "identity_features": [
            {
              "feature": { "name": "mv_feature" },
              "feature_state_value": "override-from-second-entry",
              "enabled": true
            }
          ]
        }
      ]
    }
    

    After using pnpm patch and applying the PR changes myself, the issue was fixed, and we can now use all the overrides set in the admin console.

    I will try to reproduce this myself with a free account at some point this week or next week

  5. matthewelwell commented on May 19, 2026

    @matthewelwell
    Contributor

    Interesting! Thanks for the detailed response @xolott-ark.

    I am latching on to this part of your response: "Confirmed flagsmith is correctly paginating the endpoint." - can you confirm that the /api/v1/environment-document endpoint for your environment returns an incomplete response (i.e. the response returns the Link header)?

    If so, I wonder if this schema mismatch occurs when an identity's overrides span a pagination boundary. That will give me something to investigate on our end.

  6. xolott-ark commented on May 19, 2026

    @xolott-ark
    Author

    @matthewelwell yes, it returns this:

    link: </api/v1/environment-document/?page_id=identity_override%3A130469%3A${SOME_UUID}>; rel="next"
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions