Skip to content

Commit db55ef5

Browse files
authored
feat: Identity overrides in local evaluation mode (#142)
* feat: Identity overrides in local evaluation mode - Store environment-supplied identity overrides - Use stored identity overrides in local evaluation mode - Remove integration configurations from `EnvironmentModel`
1 parent b030b37 commit db55ef5

File tree

6 files changed

+184
-77
lines changed

6 files changed

+184
-77
lines changed

src/main/java/com/flagsmith/FlagsmithClient.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class FlagsmithClient {
3939
private FlagsmithSdk flagsmithSdk;
4040
private EnvironmentModel environment;
4141
private PollingManager pollingManager;
42+
private Map<String, IdentityModel> identitiesWithOverridesByIdentifier;
4243

4344
private FlagsmithClient() {
4445
}
@@ -57,6 +58,16 @@ public void updateEnvironment() {
5758
// if we didn't get an environment from the API,
5859
// then don't overwrite the copy we already have.
5960
if (updatedEnvironment != null) {
61+
List<IdentityModel> identityOverrides = updatedEnvironment.getIdentityOverrides();
62+
63+
if (identityOverrides != null) {
64+
Map<String, IdentityModel> identitiesWithOverridesByIdentifier = new HashMap<>();
65+
for (IdentityModel identity : identityOverrides) {
66+
identitiesWithOverridesByIdentifier.put(identity.getIdentifier(), identity);
67+
}
68+
this.identitiesWithOverridesByIdentifier = identitiesWithOverridesByIdentifier;
69+
}
70+
6071
this.environment = updatedEnvironment;
6172
} else {
6273
logger.error(getEnvironmentUpdateErrorMessage());
@@ -138,7 +149,7 @@ public List<Segment> getIdentitySegments(String identifier, Map<String, Object>
138149
if (environment == null) {
139150
throw new FlagsmithClientError("Local evaluation required to obtain identity segments.");
140151
}
141-
IdentityModel identityModel = buildIdentityModel(
152+
IdentityModel identityModel = getIdentityModel(
142153
identifier, (traits != null ? traits : new HashMap<>()));
143154
List<SegmentModel> segmentModels = SegmentEvaluator.getIdentitySegments(
144155
environment, identityModel);
@@ -187,7 +198,7 @@ private Flags getIdentityFlagsFromDocument(String identifier, Map<String, Object
187198
return getDefaultFlags();
188199
}
189200

190-
IdentityModel identity = buildIdentityModel(identifier, traits);
201+
IdentityModel identity = getIdentityModel(identifier, traits);
191202
List<FeatureStateModel> featureStates = Engine.getIdentityFeatureStates(environment, identity);
192203

193204
return Flags.fromFeatureStateModels(
@@ -245,11 +256,11 @@ private Flags getIdentityFlagsFromApi(String identifier, Map<String, Object> tra
245256
}
246257
}
247258

248-
private IdentityModel buildIdentityModel(String identifier, Map<String, Object> traits)
259+
private IdentityModel getIdentityModel(String identifier, Map<String, Object> traits)
249260
throws FlagsmithClientError {
250261
if (environment == null) {
251262
throw new FlagsmithClientError(
252-
"Unable to build identity model when no local environment present.");
263+
"Unable to build identity model when no local environment present.");
253264
}
254265

255266
List<TraitModel> traitsList = traits.entrySet().stream().map((entry) -> {
@@ -260,6 +271,14 @@ private IdentityModel buildIdentityModel(String identifier, Map<String, Object>
260271
return trait;
261272
}).collect(Collectors.toList());
262273

274+
if (identitiesWithOverridesByIdentifier != null) {
275+
IdentityModel identityOverride = identitiesWithOverridesByIdentifier.get(identifier);
276+
if (identityOverride != null) {
277+
identityOverride.updateTraits(traitsList);
278+
return identityOverride;
279+
}
280+
}
281+
263282
IdentityModel identity = new IdentityModel();
264283
identity.setIdentityTraits(traitsList);
265284
identity.setEnvironmentApiKey(environment.getApiKey());
Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
package com.flagsmith.flagengine.environments;
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
4-
import com.flagsmith.flagengine.environments.integrations.IntegrationModel;
54
import com.flagsmith.flagengine.features.FeatureStateModel;
5+
import com.flagsmith.flagengine.identities.IdentityModel;
66
import com.flagsmith.flagengine.projects.ProjectModel;
77
import com.flagsmith.utils.models.BaseModel;
88
import java.util.List;
99
import lombok.Data;
1010

11-
1211
@Data
1312
public class EnvironmentModel extends BaseModel {
1413
private Integer id;
@@ -20,12 +19,6 @@ public class EnvironmentModel extends BaseModel {
2019
@JsonProperty("feature_states")
2120
private List<FeatureStateModel> featureStates;
2221

23-
@JsonProperty("amplitude_config")
24-
private IntegrationModel amplitudeConfig;
25-
@JsonProperty("segment_config")
26-
private IntegrationModel segmentConfig;
27-
@JsonProperty("mixpanel_config")
28-
private IntegrationModel mixpanelConfig;
29-
@JsonProperty("heap_config")
30-
private IntegrationModel heapConfig;
22+
@JsonProperty("identity_overrides")
23+
private List<IdentityModel> identityOverrides;
3124
}

src/main/java/com/flagsmith/flagengine/identities/IdentityModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public class IdentityModel extends BaseModel {
2929
@JsonProperty("identity_traits")
3030
private List<TraitModel> identityTraits = new ArrayList<>();
3131
@JsonProperty("identity_features")
32-
private Set<FeatureStateModel> identityFeatures = new HashSet<>();
32+
private List<FeatureStateModel> identityFeatures = new ArrayList<>();
3333
@JsonProperty("composite_key")
3434
private String compositeKey;
3535

src/test/java/com/flagsmith/FlagsmithClientTest.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
import com.flagsmith.exceptions.FlagsmithRuntimeError;
2323
import com.flagsmith.flagengine.environments.EnvironmentModel;
2424
import com.flagsmith.flagengine.features.FeatureStateModel;
25+
import com.flagsmith.flagengine.identities.IdentityModel;
2526
import com.flagsmith.flagengine.identities.traits.TraitModel;
2627
import com.flagsmith.interfaces.FlagsmithCache;
2728
import com.flagsmith.models.BaseFlag;
2829
import com.flagsmith.models.DefaultFlag;
29-
import com.flagsmith.models.Flag;
3030
import com.flagsmith.models.Flags;
3131
import com.flagsmith.models.Segment;
3232
import com.flagsmith.responses.FlagsAndTraitsResponse;
@@ -44,9 +44,7 @@
4444
import okhttp3.ResponseBody;
4545
import okhttp3.mock.MockInterceptor;
4646
import okio.Buffer;
47-
import org.junit.Rule;
4847
import org.junit.jupiter.api.Test;
49-
import org.junit.rules.ExpectedException;
5048
import org.mockito.ArgumentCaptor;
5149
import org.mockito.Mockito;
5250
import org.mockito.invocation.Invocation;
@@ -579,6 +577,31 @@ public void testUpdateEnvironment_DoesNothing_WhenGetEnvironmentReturnsNullAndEn
579577
assertEquals(client.getEnvironment(), null);
580578
}
581579

580+
@Test
581+
public void testUpdateEnvironment_StoresIdentityOverrides_WhenGetEnvironmentReturnsEnvironmentWithOverrides() {
582+
// Given
583+
EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel();
584+
585+
FlagsmithApiWrapper mockApiWrapper = mock(FlagsmithApiWrapper.class);
586+
when(mockApiWrapper.getEnvironment()).thenReturn(environmentModel);
587+
588+
FlagsmithClient client = FlagsmithClient.newBuilder()
589+
.withFlagsmithApiWrapper(mockApiWrapper)
590+
.withConfiguration(FlagsmithConfig.newBuilder().withLocalEvaluation(true).build())
591+
.setApiKey("ser.dummy-key")
592+
.build();
593+
594+
// When
595+
client.updateEnvironment();
596+
597+
// Then
598+
// Identity overrides are correctly stored
599+
IdentityModel actualIdentity = client.getIdentitiesWithOverridesByIdentifier().get("overridden-identity");
600+
601+
assertEquals(actualIdentity.getIdentityFeatures().size(), 1);
602+
assertEquals(actualIdentity.getIdentityFeatures().iterator().next().getValue(), "overridden-value");
603+
}
604+
582605
@Test
583606
public void testClose_StopsPollingManager() {
584607
// Given
@@ -654,6 +677,35 @@ public void testLocalEvaluation_ReturnsConsistentResults() throws FlagsmithClien
654677
}
655678
}
656679

680+
@Test
681+
public void testLocalEvaluation_ReturnsIdentityOverrides() throws FlagsmithClientError {
682+
// Given
683+
EnvironmentModel environmentModel = FlagsmithTestHelper.environmentModel();
684+
685+
FlagsmithConfig config = FlagsmithConfig.newBuilder().withLocalEvaluation(true).build();
686+
687+
FlagsmithApiWrapper mockedApiWrapper = mock(FlagsmithApiWrapper.class);
688+
when(mockedApiWrapper.getEnvironment())
689+
.thenReturn(environmentModel)
690+
.thenReturn(null);
691+
when(mockedApiWrapper.getConfig()).thenReturn(config);
692+
693+
FlagsmithClient client = FlagsmithClient.newBuilder()
694+
.withFlagsmithApiWrapper(mockedApiWrapper)
695+
.withConfiguration(config)
696+
.setApiKey("ser.dummy-key")
697+
.build();
698+
699+
Flags flagsWithoutOverride = client.getIdentityFlags("test");
700+
701+
// When
702+
Flags flagsWithOverride = client.getIdentityFlags("overridden-identity");
703+
704+
// Then
705+
assertEquals(flagsWithoutOverride.getFeatureValue("some_feature"), "some-value");
706+
assertEquals(flagsWithOverride.getFeatureValue("some_feature"), "overridden-value");
707+
}
708+
657709
@Test
658710
public void testGetEnvironmentFlags_UsesDefaultFlags_IfLocalEvaluationEnvironmentNull()
659711
throws FlagsmithClientError {

src/test/java/com/flagsmith/FlagsmithTestHelper.java

Lines changed: 102 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -273,62 +273,110 @@ public static IdentityModel featureUser(String identifier) {
273273
return user;
274274
}
275275

276+
public static IdentityModel identityOverride() {
277+
final FeatureModel overriddenFeature = new FeatureModel();
278+
overriddenFeature.setId(1);
279+
overriddenFeature.setName("some_feature");
280+
overriddenFeature.setType("STANDARD");
281+
282+
final FeatureStateModel overriddenFeatureState = new FeatureStateModel();
283+
overriddenFeatureState.setFeature(overriddenFeature);
284+
overriddenFeatureState.setFeaturestateUuid("d5d0767b-6287-4bb4-9d53-8b87e5458642");
285+
overriddenFeatureState.setValue("overridden-value");
286+
overriddenFeatureState.setEnabled(true);
287+
overriddenFeatureState.setMultivariateFeatureStateValues(new ArrayList<>());
288+
289+
List<FeatureStateModel> identityFeatures = new ArrayList<>();
290+
identityFeatures.add(overriddenFeatureState);
291+
292+
final IdentityModel identity = new IdentityModel();
293+
identity.setIdentifier("overridden-identity");
294+
identity.setIdentityUuid("65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb");
295+
identity.setCompositeKey("B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity");
296+
identity.setEnvironmentApiKey("B62qaMZNwfiqT76p38ggrQ");
297+
identity.setIdentityFeatures(identityFeatures);
298+
return identity;
299+
}
300+
276301
public static String environmentString() {
277302
return "{\n" +
278-
" \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" +
279-
" \"project\": {\n" +
280-
" \"name\": \"Test project\",\n" +
281-
" \"organisation\": {\n" +
282-
" \"feature_analytics\": false,\n" +
283-
" \"name\": \"Test Org\",\n" +
284-
" \"id\": 1,\n" +
285-
" \"persist_trait_data\": true,\n" +
286-
" \"stop_serving_flags\": false\n" +
287-
" },\n" +
288-
" \"id\": 1,\n" +
289-
" \"hide_disabled_flags\": false,\n" +
290-
" \"segments\": [" +
291-
" {\n" +
292-
" \"id\": 1,\n" +
293-
" \"name\": \"Test segment\",\n" +
294-
" \"rules\": [\n" +
295-
" {\n" +
296-
" \"type\": \"ALL\",\n" +
297-
" \"rules\": [\n" +
298-
" {\n" +
299-
" \"type\": \"ALL\",\n" +
300-
" \"rules\": [],\n" +
301-
" \"conditions\": [\n" +
302-
" {\n" +
303-
" \"operator\": \"EQUAL\",\n" +
304-
" \"property_\": \"foo\",\n" +
305-
" \"value\": \"bar\"\n" +
306-
" }\n" +
307-
" ]\n" +
308-
" }\n" +
309-
" ]\n" +
310-
" }\n" +
311-
" ]\n" +
312-
" }]\n" +
313-
" },\n" +
314-
" \"segment_overrides\": [],\n" +
315-
" \"id\": 1,\n" +
316-
" \"feature_states\": [\n" +
317-
" {\n" +
318-
" \"multivariate_feature_state_values\": [],\n" +
319-
" \"feature_state_value\": \"some-value\",\n" +
320-
" \"id\": 1,\n" +
321-
" \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" +
322-
" \"feature\": {\n" +
323-
" \"name\": \"some_feature\",\n" +
324-
" \"type\": \"STANDARD\",\n" +
325-
" \"id\": 1\n" +
326-
" },\n" +
327-
" \"segment_id\": null,\n" +
328-
" \"enabled\": true\n" +
329-
" }\n" +
330-
" ]\n" +
331-
"}";
303+
" \"api_key\": \"B62qaMZNwfiqT76p38ggrQ\",\n" +
304+
" \"project\": {\n" +
305+
" \"name\": \"Test project\",\n" +
306+
" \"organisation\": {\n" +
307+
" \"feature_analytics\": false,\n" +
308+
" \"name\": \"Test Org\",\n" +
309+
" \"id\": 1,\n" +
310+
" \"persist_trait_data\": true,\n" +
311+
" \"stop_serving_flags\": false\n" +
312+
" },\n" +
313+
" \"id\": 1,\n" +
314+
" \"hide_disabled_flags\": false,\n" +
315+
" \"segments\": [\n" +
316+
" {\n" +
317+
" \"id\": 1,\n" +
318+
" \"name\": \"Test segment\",\n" +
319+
" \"rules\": [\n" +
320+
" {\n" +
321+
" \"type\": \"ALL\",\n" +
322+
" \"rules\": [\n" +
323+
" {\n" +
324+
" \"type\": \"ALL\",\n" +
325+
" \"rules\": [],\n" +
326+
" \"conditions\": [\n" +
327+
" {\n" +
328+
" \"operator\": \"EQUAL\",\n" +
329+
" \"property_\": \"foo\",\n" +
330+
" \"value\": \"bar\"\n" +
331+
" }\n" +
332+
" ]\n" +
333+
" }\n" +
334+
" ]\n" +
335+
" }\n" +
336+
" ]\n" +
337+
" }\n" +
338+
" ]\n" +
339+
" },\n" +
340+
" \"segment_overrides\": [],\n" +
341+
" \"id\": 1,\n" +
342+
" \"feature_states\": [\n" +
343+
" {\n" +
344+
" \"multivariate_feature_state_values\": [],\n" +
345+
" \"feature_state_value\": \"some-value\",\n" +
346+
" \"id\": 1,\n" +
347+
" \"featurestate_uuid\": \"40eb539d-3713-4720-bbd4-829dbef10d51\",\n" +
348+
" \"feature\": {\n" +
349+
" \"name\": \"some_feature\",\n" +
350+
" \"type\": \"STANDARD\",\n" +
351+
" \"id\": 1\n" +
352+
" },\n" +
353+
" \"segment_id\": null,\n" +
354+
" \"enabled\": true\n" +
355+
" }\n" +
356+
" ],\n" +
357+
" \"identity_overrides\": [\n" +
358+
" {\n" +
359+
" \"identity_uuid\": \"65bc5ac6-5859-4cfe-97e6-d5ec2e80c1fb\",\n" +
360+
" \"identifier\": \"overridden-identity\",\n" +
361+
" \"composite_key\": \"B62qaMZNwfiqT76p38ggrQ_identity_overridden_identity\",\n" +
362+
" \"identity_features\": [\n" +
363+
" {\n" +
364+
" \"feature_state_value\": \"overridden-value\",\n" +
365+
" \"multivariate_feature_state_values\": [],\n" +
366+
" \"featurestate_uuid\": \"d5d0767b-6287-4bb4-9d53-8b87e5458642\",\n" +
367+
" \"feature\": {\n" +
368+
" \"name\": \"some_feature\",\n" +
369+
" \"type\": \"STANDARD\",\n" +
370+
" \"id\": 1\n" +
371+
" },\n" +
372+
" \"enabled\": true\n" +
373+
" }\n" +
374+
" ],\n" +
375+
" \"identity_traits\": [],\n" +
376+
" \"environment_api_key\": \"B62qaMZNwfiqT76p38ggrQ\"\n" +
377+
" }\n" +
378+
" ]\n" +
379+
"}";
332380
}
333381

334382
public static EnvironmentModel environmentModel() {

src/test/java/com/flagsmith/flagengine/unit/environments/EnvironmentTest.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,6 @@ public void test_get_flags_for_environment_returns_feature_states_for_environmen
6666

6767
Assertions.assertTrue(environmentModel.getFeatureStates().size() == 3);
6868

69-
Assertions.assertNull(environmentModel.getAmplitudeConfig());
70-
Assertions.assertNull(environmentModel.getMixpanelConfig());
71-
Assertions.assertNull(environmentModel.getHeapConfig());
72-
Assertions.assertNull(environmentModel.getSegmentConfig());
73-
7469
FeatureStateModel featureState = FeatureStateHelper.getFeatureStateForFeatureByName(
7570
environmentModel.getFeatureStates(),
7671
"feature_with_string_value"

0 commit comments

Comments
 (0)