diff --git a/sdk/index.ts b/sdk/index.ts index 72d5f9d..2cbe970 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -23,6 +23,7 @@ import { TraitModel, getEvaluationResult } from '../flagsmith-engine/index.js'; +import { IdentityFeaturesList } from '../flagsmith-engine/utils/collections.js'; import { Fetch, FlagsmithCache, @@ -296,6 +297,27 @@ export class Flagsmith { return SegmentModel.fromSegmentResult(evaluationResult.segments, context); } + private parseIdentityOverrides(environment: EnvironmentModel) { + this.identitiesWithOverridesByIdentifier = new Map(); + for (const identity of environment.identityOverrides) { + const existing = this.identitiesWithOverridesByIdentifier.get(identity.identifier); + if (existing) { + existing.identityFeatures.push(...(identity.identityFeatures || [])); + } else { + const clone = Object.assign( + Object.create(Object.getPrototypeOf(identity)) as IdentityModel, + identity, + { + identityFeatures: new IdentityFeaturesList( + ...(identity.identityFeatures || []) + ) + } + ); + this.identitiesWithOverridesByIdentifier.set(identity.identifier, clone); + } + } + } + private async fetchEnvironment(): Promise { const deferred = new Deferred(); this.environmentPromise = deferred.promise; @@ -303,9 +325,7 @@ export class Flagsmith { const environment = await this.getEnvironmentFromApi(); this.environment = environment; if (environment.identityOverrides?.length) { - this.identitiesWithOverridesByIdentifier = new Map( - environment.identityOverrides.map(identity => [identity.identifier, identity]) - ); + this.parseIdentityOverrides(environment); } deferred.resolve(environment); return deferred.promise; diff --git a/tests/engine/unit/mappers.test.ts b/tests/engine/unit/mappers.test.ts index 9c6d431..c475e5d 100644 --- a/tests/engine/unit/mappers.test.ts +++ b/tests/engine/unit/mappers.test.ts @@ -336,8 +336,9 @@ describe('getEvaluationContext', () => { }); test('handles multiple identity overrides with same features', () => { - // Given - the fixture already has identity override with 'overridden-id' - // Verify it's mapped correctly + // Given - the fixture has identity overrides for 'overridden-id' (some_feature) + // plus two entries for 'multi-override-id' covering some_feature and mv_feature. + // Each unique feature-set hash produces its own identity override segment. const context = getEvaluationContext(testEnvironment); // Then @@ -345,9 +346,15 @@ describe('getEvaluationContext', () => { s => s.name === IDENTITY_OVERRIDE_SEGMENT_NAME ); - // The fixture has one identity override - expect(overrideSegments.length).toBe(1); - expect(overrideSegments[0].rules?.[0].conditions?.[0].value).toContain('overridden-id'); - expect(overrideSegments[0].overrides?.length).toBe(1); + expect(overrideSegments.length).toBe(3); + + const identifiers = overrideSegments + .map(s => s.rules?.[0].conditions?.[0].value as string) + .sort(); + expect(identifiers).toEqual(['multi-override-id', 'multi-override-id', 'overridden-id']); + + for (const segment of overrideSegments) { + expect(segment.overrides?.length).toBe(1); + } }); }); diff --git a/tests/sdk/data/environment.json b/tests/sdk/data/environment.json index df92cd1..224e956 100644 --- a/tests/sdk/data/environment.json +++ b/tests/sdk/data/environment.json @@ -114,6 +114,52 @@ "feature_segment": null } ] + }, + { + "identifier": "multi-override-id", + "identity_uuid": "11111111-1111-1111-1111-111111111111", + "created_date": "2024-01-01T00:00:00.000000Z", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_traits": [], + "identity_features": [ + { + "id": 100, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "feature_state_value": "override-from-first-entry", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + }, + { + "identifier": "multi-override-id", + "identity_uuid": "11111111-1111-1111-1111-111111111111", + "created_date": "2024-01-02T00:00:00.000000Z", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_traits": [], + "identity_features": [ + { + "id": 101, + "feature": { + "id": 2, + "name": "mv_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "feature_state_value": "override-from-second-entry", + "enabled": true, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] } ] } \ No newline at end of file diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index be64a1d..c525b55 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -498,6 +498,21 @@ test('test_localEvaluation_true__identity_overrides_evaluated', async () => { expect(flags.getFeatureValue('some_feature')).toEqual('some-overridden-value'); }); +test('test_localEvaluation_true__multiple_identity_overrides_for_same_identifier_merged', async () => { + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true + }); + + await flg.updateEnvironment(); + const flags = await flg.getIdentityFlags('multi-override-id'); + + expect(flags.getFeatureValue('some_feature')).toEqual('override-from-first-entry'); + expect(flags.isFeatureEnabled('some_feature')).toEqual(false); + expect(flags.getFeatureValue('mv_feature')).toEqual('override-from-second-entry'); + expect(flags.isFeatureEnabled('mv_feature')).toEqual(true); +}); + test('getIdentityFlags succeeds if initial fetch failed then succeeded', async () => { const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', true));