Skip to content

Commit 4d987c6

Browse files
authored
feat: Support transient identities and traits (#158)
1 parent fa73f40 commit 4d987c6

File tree

15 files changed

+326
-83
lines changed

15 files changed

+326
-83
lines changed

src/main/java/com/flagsmith/FlagsmithApiWrapper.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,33 +172,36 @@ public Flags getFeatureFlags(boolean doThrow) {
172172

173173
@Override
174174
public Flags identifyUserWithTraits(
175-
String identifier, List<TraitModel> traits, boolean doThrow
175+
String identifier, List<? extends TraitModel> traits, boolean isTransient, boolean doThrow
176176
) {
177177
assertValidUser(identifier);
178178
Flags flags = null;
179179
String cacheKey = null;
180180

181181
if (getCache() != null) {
182-
cacheKey = getCache().getIdentityFlagsCacheKey(identifier);
182+
cacheKey = getCache().getIdentityFlagsCacheKey(identifier, isTransient);
183183
flags = getCache().getIfPresent(cacheKey);
184184

185185
if (flags != null) {
186186
return flags;
187187
}
188188
}
189189

190-
// we are using identities endpoint to create bulk user Trait
191-
HttpUrl url = defaultConfig.getIdentitiesUri();
192-
193190
ObjectNode node = MapperFactory.getMapper().createObjectNode();
194191
node.put("identifier", identifier);
195192

193+
if (isTransient) {
194+
node.put("transient", true);
195+
}
196+
196197
if (traits != null) {
197198
node.putPOJO("traits", traits);
198199
}
199200

200201
MediaType json = MediaType.parse("application/json; charset=utf-8");
201-
RequestBody body = RequestBody.create(json, node.toString());
202+
RequestBody body = RequestBody.create(node.toString(), json);
203+
204+
HttpUrl url = defaultConfig.getIdentitiesUri();
202205

203206
final Request request = this.newPostRequest(url, body);
204207

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

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
import com.flagsmith.interfaces.FlagsmithSdk;
1717
import com.flagsmith.models.BaseFlag;
1818
import com.flagsmith.models.Flags;
19+
import com.flagsmith.models.SdkTraitModel;
1920
import com.flagsmith.models.Segment;
2021
import com.flagsmith.threads.PollingManager;
22+
import com.flagsmith.utils.ModelUtils;
23+
import java.util.ArrayList;
2124
import java.util.HashMap;
2225
import java.util.List;
2326
import java.util.Map;
@@ -78,6 +81,11 @@ public void updateEnvironment() {
7881

7982
/**
8083
* Get all the default for flags for the current environment.
84+
* <<<<<<< HEAD
85+
* =======
86+
*
87+
* @return environment flags
88+
* >>>>>>> a7a9291 (feat: Support transient identities and traits)
8189
*/
8290
public Flags getEnvironmentFlags() throws FlagsmithClientError {
8391
if (getShouldUseEnvironmentDocument()) {
@@ -88,12 +96,13 @@ public Flags getEnvironmentFlags() throws FlagsmithClientError {
8896
}
8997

9098
/**
91-
* Get all the flags for the current environment for a given identity. Will also
92-
* upsert all traits to the Flagsmith API for future evaluations. Providing a
93-
* trait with a value of None will remove the trait from the identity if it
94-
* exists.
99+
* Get all the flags for the current environment for a given identity.
95100
*
96101
* @param identifier identifier string
102+
* <<<<<<< HEAD
103+
* =======
104+
* @return result of flag evaluation for given identity
105+
* >>>>>>> a7a9291 (feat: Support transient identities and traits)
97106
*/
98107
public Flags getIdentityFlags(String identifier)
99108
throws FlagsmithClientError {
@@ -102,20 +111,58 @@ public Flags getIdentityFlags(String identifier)
102111

103112
/**
104113
* Get all the flags for the current environment for a given identity. Will also
105-
* upsert all traits to the Flagsmith API for future evaluations. Providing a
106-
* trait with a value of None will remove the trait from the identity if it
114+
* upsert traits to the Flagsmith API for future evaluations.
115+
*
116+
* <p>
117+
* A trait with a value of null will remove the trait from the identity if it
107118
* exists.
119+
* </p>
120+
* <p>
121+
* To specify a transient trait, use the TraitConfig class with isTransient set
122+
* to true as the trait value.
123+
* </p>
108124
*
125+
* @see com.flagsmith.models.TraitConfig
126+
*
109127
* @param identifier identifier string
110-
* @param traits list of key value traits
128+
* @param traits a map of trait keys to trait values
111129
*/
112130
public Flags getIdentityFlags(String identifier, Map<String, Object> traits)
113131
throws FlagsmithClientError {
132+
return getIdentityFlags(identifier, traits, false);
133+
}
134+
135+
/**
136+
* Get all the flags for the current environment for a given identity. Will also
137+
* upsert traits to the Flagsmith API for future evaluations, if isTransient set
138+
* to false.
139+
*
140+
* <p>
141+
* A trait with a value of null will remove the trait from the identity if it
142+
* exists.
143+
* </p>
144+
* <p>
145+
* To specify a transient trait, use the TraitConfig class with isTransient set
146+
* to true as the trait value.
147+
* </p>
148+
*
149+
* @see com.flagsmith.models.TraitConfig
150+
*
151+
* @param identifier identifier string
152+
* @param traits a map of trait keys to trait values
153+
* @param isTransient set to true to prevent identity persistence
154+
* @return result of flag evaluation for given identity
155+
*/
156+
public Flags getIdentityFlags(String identifier, Map<String, Object> traits, boolean isTransient)
157+
throws FlagsmithClientError {
114158
if (getShouldUseEnvironmentDocument()) {
115-
return getIdentityFlagsFromDocument(identifier, traits);
159+
return getIdentityFlagsFromDocument(
160+
identifier,
161+
ModelUtils.getTraitModelsFromTraitMap(traits));
116162
}
117163

118-
return getIdentityFlagsFromApi(identifier, traits);
164+
return getIdentityFlagsFromApi(
165+
identifier, ModelUtils.getSdkTraitModelsFromTraitMap(traits), isTransient);
119166
}
120167

121168
/**
@@ -143,7 +190,10 @@ public List<Segment> getIdentitySegments(String identifier, Map<String, Object>
143190
throw new FlagsmithClientError("Local evaluation required to obtain identity segments.");
144191
}
145192
IdentityModel identityModel = getIdentityModel(
146-
identifier, (traits != null ? traits : new HashMap<>()));
193+
identifier,
194+
(traits != null
195+
? ModelUtils.getTraitModelsFromTraitMap(traits)
196+
: new ArrayList<TraitModel>()));
147197
List<SegmentModel> segmentModels = SegmentEvaluator.getIdentitySegments(
148198
environment, identityModel);
149199

@@ -182,7 +232,8 @@ private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError {
182232
getConfig().getFlagsmithFlagDefaults());
183233
}
184234

185-
private Flags getIdentityFlagsFromDocument(String identifier, Map<String, Object> traits)
235+
private Flags getIdentityFlagsFromDocument(
236+
String identifier, List<? extends TraitModel> traitModels)
186237
throws FlagsmithClientError {
187238
if (environment == null) {
188239
if (getConfig().getFlagsmithFlagDefaults() == null) {
@@ -191,7 +242,7 @@ private Flags getIdentityFlagsFromDocument(String identifier, Map<String, Object
191242
return getDefaultFlags();
192243
}
193244

194-
IdentityModel identity = getIdentityModel(identifier, traits);
245+
IdentityModel identity = getIdentityModel(identifier, traitModels);
195246
List<FeatureStateModel> featureStates = Engine.getIdentityFeatureStates(environment, identity);
196247

197248
return Flags.fromFeatureStateModels(
@@ -219,27 +270,21 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError {
219270
}
220271
}
221272

222-
private Flags getIdentityFlagsFromApi(String identifier, Map<String, Object> traits)
273+
private Flags getIdentityFlagsFromApi(
274+
String identifier, List<SdkTraitModel> traitModels, boolean isTransient)
223275
throws FlagsmithApiError {
224276
try {
225-
List<TraitModel> traitsList = traits.entrySet().stream().map((row) -> {
226-
TraitModel trait = new TraitModel();
227-
trait.setTraitValue(row.getValue());
228-
trait.setTraitKey(row.getKey());
229-
230-
return trait;
231-
}).collect(Collectors.toList());
232-
233277
return flagsmithSdk.identifyUserWithTraits(
234278
identifier,
235-
traitsList,
279+
traitModels,
280+
isTransient,
236281
Boolean.TRUE);
237282
} catch (Exception e) {
238283
if (getConfig().getFlagsmithFlagDefaults() != null) {
239284
return getDefaultFlags();
240285
} else if (environment != null) {
241286
try {
242-
return getIdentityFlagsFromDocument(identifier, traits);
287+
return getIdentityFlagsFromDocument(identifier, traitModels);
243288
} catch (FlagsmithClientError ce) {
244289
// Do nothing and fall through to FlagsmithApiError
245290
}
@@ -249,31 +294,23 @@ private Flags getIdentityFlagsFromApi(String identifier, Map<String, Object> tra
249294
}
250295
}
251296

252-
private IdentityModel getIdentityModel(String identifier, Map<String, Object> traits)
297+
private IdentityModel getIdentityModel(String identifier, List<? extends TraitModel> traitModels)
253298
throws FlagsmithClientError {
254299
if (environment == null) {
255300
throw new FlagsmithClientError(
256301
"Unable to build identity model when no local environment present.");
257302
}
258303

259-
List<TraitModel> traitsList = traits.entrySet().stream().map((entry) -> {
260-
TraitModel trait = new TraitModel();
261-
trait.setTraitKey(entry.getKey());
262-
trait.setTraitValue(entry.getValue());
263-
264-
return trait;
265-
}).collect(Collectors.toList());
266-
267304
if (identitiesWithOverridesByIdentifier != null) {
268305
IdentityModel identityOverride = identitiesWithOverridesByIdentifier.get(identifier);
269306
if (identityOverride != null) {
270-
identityOverride.updateTraits(traitsList);
307+
identityOverride.updateTraits(traitModels);
271308
return identityOverride;
272309
}
273310
}
274311

275312
IdentityModel identity = new IdentityModel();
276-
identity.setIdentityTraits(traitsList);
313+
identity.setIdentityTraits(traitModels);
277314
identity.setEnvironmentApiKey(environment.getApiKey());
278315
identity.setIdentifier(identifier);
279316

@@ -289,7 +326,7 @@ private Flags getDefaultFlags() {
289326
private String getEnvironmentUpdateErrorMessage() {
290327
if (this.environment == null) {
291328
return "Unable to update environment from API. "
292-
+ "No environment configured - using defaultHandler if configured.";
329+
+ "No environment configured - using defaultHandler if configured.";
293330
} else {
294331
return "Unable to update environment from API. Continuing to use previous copy.";
295332
}
@@ -541,7 +578,7 @@ public FlagsmithClient build() {
541578
if (configuration.getOfflineHandler() != null) {
542579
if (configuration.getFlagsmithFlagDefaults() != null) {
543580
throw new FlagsmithRuntimeError(
544-
"Cannot use both default flag handler and offline handler.");
581+
"Cannot use both default flag handler and offline handler.");
545582
}
546583
client.environment = configuration.getOfflineHandler().getEnvironment();
547584
}

src/main/java/com/flagsmith/config/FlagsmithCacheConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ public String getEnvFlagsCacheKey() {
206206
}
207207

208208
@Override
209-
public String getIdentityFlagsCacheKey(String identifier) {
210-
return "identity" + identifier;
209+
public String getIdentityFlagsCacheKey(String identifier, boolean isTransient) {
210+
return "identity" + identifier + (isTransient ? "transient" : "");
211211
}
212212

213213
@Override

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public class IdentityModel extends BaseModel {
2727
@JsonProperty("identity_uuid")
2828
private String identityUuid = UUID.randomUUID().toString();
2929
@JsonProperty("identity_traits")
30-
private List<TraitModel> identityTraits = new ArrayList<>();
30+
private List<? extends TraitModel> identityTraits = new ArrayList<>();
3131
@JsonProperty("identity_features")
3232
private List<FeatureStateModel> identityFeatures = new ArrayList<>();
3333
@JsonProperty("composite_key")
@@ -48,7 +48,7 @@ public String getCompositeKey() {
4848
*
4949
* @param traits traits to update
5050
*/
51-
public void updateTraits(List<TraitModel> traits) {
51+
public void updateTraits(List<? extends TraitModel> traits) {
5252
Map<String, TraitModel> existingTraits = new HashMap<>();
5353

5454
if (identityTraits != null && identityTraits.size() > 0) {

src/main/java/com/flagsmith/flagengine/identities/traits/TraitModel.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import lombok.AllArgsConstructor;
55
import lombok.Data;
66
import lombok.NoArgsConstructor;
7+
import lombok.experimental.SuperBuilder;
78

89
@Data
910
@NoArgsConstructor
1011
@AllArgsConstructor
12+
@SuperBuilder
1113
public class TraitModel {
1214
@JsonProperty("trait_key")
1315
private String traitKey;

src/main/java/com/flagsmith/flagengine/segments/SegmentEvaluator.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public static List<SegmentModel> getIdentitySegments(EnvironmentModel environmen
5151
* @param overrideTraits Overriden traits.
5252
*/
5353
public static Boolean evaluateIdentityInSegment(IdentityModel identity, SegmentModel segment,
54-
List<TraitModel> overrideTraits) {
54+
List<? extends TraitModel> overrideTraits) {
5555
List<SegmentRuleModel> segmentRules = segment.getRules();
56-
List<TraitModel> traits =
56+
List<? extends TraitModel> traits =
5757
overrideTraits != null ? overrideTraits : identity.getIdentityTraits();
5858

5959
String identityHashKey = identity.getDjangoId() == null ? identity.getCompositeKey()
@@ -83,7 +83,7 @@ public static Boolean evaluateIdentityInSegment(IdentityModel identity, SegmentM
8383
* @param segmentId Segment ID (for hashing)
8484
* @param identityId Identity ID (for hashing)
8585
*/
86-
private static Boolean traitsMatchSegmentRule(List<TraitModel> identityTraits,
86+
private static Boolean traitsMatchSegmentRule(List<? extends TraitModel> identityTraits,
8787
SegmentRuleModel rule,
8888
Integer segmentId, String identityId) {
8989
Boolean matchingCondition = Boolean.TRUE;
@@ -122,7 +122,7 @@ private static Boolean traitsMatchSegmentRule(List<TraitModel> identityTraits,
122122
* @param segmentId Segment ID (for hashing)
123123
* @param identityId Identity ID (for hashing)
124124
*/
125-
private static Boolean traitsMatchSegmentCondition(List<TraitModel> identityTraits,
125+
private static Boolean traitsMatchSegmentCondition(List<? extends TraitModel> identityTraits,
126126
SegmentConditionModel condition,
127127
Integer segmentId, String identityId) {
128128
if (condition.getOperator().equals(SegmentConditions.PERCENTAGE_SPLIT)) {
@@ -137,7 +137,7 @@ private static Boolean traitsMatchSegmentCondition(List<TraitModel> identityTrai
137137
}
138138

139139
if (identityTraits != null) {
140-
Optional<TraitModel> matchingTrait = identityTraits
140+
Optional<? extends TraitModel> matchingTrait = identityTraits
141141
.stream()
142142
.filter((trait) -> trait.getTraitKey().equals(condition.getProperty_()))
143143
.findFirst();
@@ -154,7 +154,7 @@ private static Boolean traitsMatchSegmentCondition(List<TraitModel> identityTrai
154154
* @param trait Trait to match against.
155155
* @param condition Condition to evaluate with.
156156
*/
157-
private static Boolean traitMatchesSegmentCondition(Optional<TraitModel> trait,
157+
private static Boolean traitMatchesSegmentCondition(Optional<? extends TraitModel> trait,
158158
SegmentConditionModel condition) {
159159
if (condition.getOperator().equals(SegmentConditions.IS_NOT_SET)) {
160160
return !trait.isPresent();

src/main/java/com/flagsmith/interfaces/FlagsmithCache.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public interface FlagsmithCache {
7272
*
7373
* @return string
7474
*/
75-
String getIdentityFlagsCacheKey(String identifier);
75+
String getIdentityFlagsCacheKey(String identifier, boolean isTransient);
7676

7777
/**
7878
* Returns the Cache instance.

src/main/java/com/flagsmith/interfaces/FlagsmithSdk.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public interface FlagsmithSdk {
1616
Flags getFeatureFlags(boolean doThrow);
1717

1818
Flags identifyUserWithTraits(
19-
String identifier, List<TraitModel> traits, boolean doThrow
19+
String identifier, List<? extends TraitModel> traits, boolean isTransient, boolean doThrow
2020
);
2121

2222
FlagsmithConfig getConfig();
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.flagsmith.models;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import com.flagsmith.flagengine.identities.traits.TraitModel;
7+
import lombok.Builder;
8+
import lombok.Data;
9+
import lombok.EqualsAndHashCode;
10+
import lombok.NoArgsConstructor;
11+
import lombok.experimental.SuperBuilder;
12+
13+
@Data
14+
@SuperBuilder
15+
@NoArgsConstructor
16+
@EqualsAndHashCode(callSuper = true)
17+
@JsonInclude(Include.NON_DEFAULT)
18+
public class SdkTraitModel extends TraitModel {
19+
@JsonProperty("transient")
20+
@Builder.Default
21+
private Boolean isTransient = false;
22+
}

0 commit comments

Comments
 (0)