Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
14bf7cc
wip
khvn26 Sep 15, 2025
da5b62b
mapper wip
khvn26 Sep 15, 2025
f989a8c
mapper fixes
khvn26 Sep 15, 2025
05abbe9
wip + offline handlers
khvn26 Sep 15, 2025
9f25722
wip + working build
khvn26 Sep 15, 2025
8be5116
wip + fix build errors for tests
khvn26 Sep 15, 2025
59583be
test fixes
khvn26 Sep 15, 2025
c55c339
wip + fix null contextValue
khvn26 Sep 15, 2025
f72f1ff
use correct module
khvn26 Sep 15, 2025
e697b61
wip + fix segment tests
khvn26 Sep 15, 2025
4dcdaed
fix assertEquals
khvn26 Sep 15, 2025
7be7e3d
wip + fix `django_id`
khvn26 Sep 15, 2025
c4750b4
fix engine and evaluator tests
khvn26 Sep 16, 2025
15155e6
build success
khvn26 Sep 16, 2025
bf4dddf
drop Java 8 support
khvn26 Sep 16, 2025
9d4b0d8
maybe use git url
khvn26 Sep 16, 2025
afdaec0
bump submodule
khvn26 Sep 16, 2025
6a7e1c3
delete more code
khvn26 Sep 16, 2025
c742ec3
minimise formatting changes
khvn26 Sep 16, 2025
89f420a
minimise formatting changes pt. 2
khvn26 Sep 16, 2025
d4e5b1e
minimise formatting changes pt. 3
khvn26 Sep 16, 2025
d99e9d1
improve formatting, comment
khvn26 Sep 16, 2025
b861a72
minimise formatting changes pt. 4
khvn26 Sep 16, 2025
4730f9a
remove debugging code
khvn26 Sep 16, 2025
bebac37
use latest schemas and engine-test-data
khvn26 Oct 9, 2025
c3a8811
cleanup
khvn26 Oct 9, 2025
bef6681
use isEmpty
khvn26 Oct 9, 2025
b0ad7d7
improve identity override segments docs
khvn26 Oct 9, 2025
d12ef3f
improve identity overrides segments keys
khvn26 Oct 9, 2025
ec6000e
use stringListTypeRef
khvn26 Oct 9, 2025
db248dd
refactor getEvaluationResult, add priority constants
khvn26 Oct 9, 2025
c4d35e2
improve docs
khvn26 Oct 10, 2025
0e827fc
improve readability
khvn26 Oct 10, 2025
3819cd9
Support segment metadata
khvn26 Oct 15, 2025
de0362b
improve naming
khvn26 Oct 15, 2025
b8ae5ab
support variant priority
khvn26 Oct 15, 2025
7764097
prevent illegal state
khvn26 Oct 15, 2025
7303aeb
clarify context value getter behaviour
khvn26 Oct 15, 2025
c37164b
fix type casting
khvn26 Oct 15, 2025
50b4bec
fix boolean coercion
khvn26 Oct 15, 2025
b4ee7ff
add test names
khvn26 Oct 15, 2025
540832f
use latest test data
khvn26 Oct 15, 2025
60d5d9e
cleanup
khvn26 Oct 15, 2025
e7cfad5
bump maven-surefire-plugin
khvn26 Oct 15, 2025
c5821d9
bring models back
khvn26 Oct 15, 2025
711ff13
...but they don't belong to the engine anymore
khvn26 Oct 15, 2025
27b4b8e
restore IOfflineHandler
khvn26 Oct 15, 2025
000e96c
cleanup to improve diff
khvn26 Oct 16, 2025
211c5bd
support feature metadata, unset segments in context
khvn26 Oct 16, 2025
8389a90
remove extra FeatureStateModel
khvn26 Oct 16, 2025
69f00a4
support setting Flag.featureId via metadata
khvn26 Oct 16, 2025
b5889e8
fix
khvn26 Oct 16, 2025
3a05d8b
fix `List<String>` coercion
khvn26 Oct 22, 2025
ed5e516
improve clarity for `IN`
khvn26 Oct 22, 2025
23799be
remove extra hop
khvn26 Oct 22, 2025
988d59b
improve naming
khvn26 Oct 22, 2025
2dfcbbc
remove extra methods
khvn26 Oct 22, 2025
183cdf9
remove null check
khvn26 Oct 22, 2025
7d1c948
improve test
khvn26 Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
java: [ "8", "11", "17", "21" ]
java: [ "11", "17", "21" ]
distribution: [ "zulu", "adopt" ]

steps:
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"]
path = src/test/java/com/flagsmith/flagengine/enginetestdata
url = [email protected]:Flagsmith/engine-test-data.git
branch = v1.0.0
branch = feat/context-values
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should bump v2.0.0 in engine-test-data?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bebac37 (the branch is based on latest main).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making sure we're in the same page here: ultimately this need to be merged as a version tag, since feat/fix branches are ephemeral. If that's correct, I'll keep this thread unresolved as a reminder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now done.

43 changes: 39 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
</scm>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.11</java.version>
<java.version>11</java.version>
<jacksonVersion>2.15.2</jacksonVersion>
<lombok.version>1.18.34</lombok.version>
<slf4j.version>1.7.30</slf4j.version>
Expand All @@ -54,6 +56,11 @@
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
Expand Down Expand Up @@ -181,6 +188,27 @@
<build>
<finalName>flagsmith-java-client-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
<version>1.2.2</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceType>jsonschema</sourceType>
<sourceDirectory>${project.basedir}/src/main/resources/schema</sourceDirectory>
<targetPackage>com.flagsmith.flagengine</targetPackage>
<outputDirectory>${project.build.directory}/generated-sources/jsonschema2pojo</outputDirectory>
<generateBuilders>true</generateBuilders>
<includeConstructors>true</includeConstructors>
<includeCopyConstructor>true</includeCopyConstructor>
</configuration>
</plugin>
<plugin>
<groupId>org.sonatype.central</groupId>
<artifactId>central-publishing-maven-plugin</artifactId>
Expand All @@ -196,8 +224,15 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>11</source>
<target>11</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -316,4 +351,4 @@
</build>
</profile>
</profiles>
</project>
</project>
20 changes: 12 additions & 8 deletions src/main/java/com/flagsmith/FlagsmithApiWrapper.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package com.flagsmith;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.flagsmith.config.FlagsmithConfig;
import com.flagsmith.exceptions.FlagsmithRuntimeError;
import com.flagsmith.flagengine.environments.EnvironmentModel;
import com.flagsmith.flagengine.features.FeatureStateModel;
import com.flagsmith.flagengine.identities.traits.TraitModel;
import com.flagsmith.flagengine.EvaluationContext;
import com.flagsmith.interfaces.FlagsmithCache;
import com.flagsmith.interfaces.FlagsmithSdk;
import com.flagsmith.mappers.EngineMappers;
import com.flagsmith.models.FeatureStateModel;
import com.flagsmith.models.Flags;
import com.flagsmith.models.TraitModel;
import com.flagsmith.responses.FlagsAndTraitsResponse;
import com.flagsmith.threads.AnalyticsProcessor;
import com.flagsmith.threads.RequestProcessor;
Expand All @@ -20,7 +22,6 @@
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import lombok.Data;
import lombok.Getter;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
Expand Down Expand Up @@ -248,19 +249,22 @@ public Flags identifyUserWithTraits(
}

@Override
public EnvironmentModel getEnvironment() {
public EvaluationContext getEvaluationContext() {
final Request request = newGetRequest(defaultConfig.getEnvironmentUri());

Future<EnvironmentModel> environmentFuture = requestor.executeAsync(request,
new TypeReference<EnvironmentModel>() {},
Future<JsonNode> environmentFuture = requestor.executeAsync(request,
new TypeReference<JsonNode>() {},
Boolean.TRUE);

try {
return environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS);
JsonNode environmentJson = environmentFuture.get(TIMEOUT, TimeUnit.MILLISECONDS);
return EngineMappers.mapEnvironmentDocumentToContext(environmentJson);
} catch (TimeoutException ie) {
logger.error("Timed out on fetching Feature flags.", ie);
} catch (InterruptedException ie) {
logger.error("Environment loading interrupted.", ie);
} catch (IllegalArgumentException iae) {
logger.error("Environment loading failed.", iae);
} catch (ExecutionException ee) {
logger.error("Execution failed on Environment loading.", ee);
throw new FlagsmithRuntimeError(ee);
Expand Down
118 changes: 38 additions & 80 deletions src/main/java/com/flagsmith/FlagsmithClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@
import com.flagsmith.exceptions.FlagsmithClientError;
import com.flagsmith.exceptions.FlagsmithRuntimeError;
import com.flagsmith.flagengine.Engine;
import com.flagsmith.flagengine.environments.EnvironmentModel;
import com.flagsmith.flagengine.features.FeatureStateModel;
import com.flagsmith.flagengine.identities.IdentityModel;
import com.flagsmith.flagengine.identities.traits.TraitModel;
import com.flagsmith.flagengine.segments.SegmentEvaluator;
import com.flagsmith.flagengine.segments.SegmentModel;
import com.flagsmith.flagengine.EvaluationContext;
import com.flagsmith.flagengine.EvaluationResult;
import com.flagsmith.interfaces.FlagsmithCache;
import com.flagsmith.interfaces.FlagsmithSdk;
import com.flagsmith.mappers.EngineMappers;
import com.flagsmith.models.BaseFlag;
import com.flagsmith.models.Flags;
import com.flagsmith.models.SdkTraitModel;
import com.flagsmith.models.Segment;
import com.flagsmith.threads.PollingManager;
import com.flagsmith.utils.ModelUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -39,9 +34,8 @@ public class FlagsmithClient {

private final FlagsmithLogger logger = new FlagsmithLogger();
private FlagsmithSdk flagsmithSdk;
private EnvironmentModel environment;
private EvaluationContext evaluationContext;
private PollingManager pollingManager;
private Map<String, IdentityModel> identitiesWithOverridesByIdentifier;

private FlagsmithClient() {
}
Expand All @@ -55,22 +49,12 @@ public static FlagsmithClient.Builder newBuilder() {
*/
public void updateEnvironment() {
try {
EnvironmentModel updatedEnvironment = flagsmithSdk.getEnvironment();
EvaluationContext updatedEvaluationContext = flagsmithSdk.getEvaluationContext();

// if we didn't get an environment from the API,
// then don't overwrite the copy we already have.
if (updatedEnvironment != null) {
List<IdentityModel> identityOverrides = updatedEnvironment.getIdentityOverrides();

if (identityOverrides != null) {
Map<String, IdentityModel> identitiesWithOverridesByIdentifier = new HashMap<>();
for (IdentityModel identity : identityOverrides) {
identitiesWithOverridesByIdentifier.put(identity.getIdentifier(), identity);
}
this.identitiesWithOverridesByIdentifier = identitiesWithOverridesByIdentifier;
}

this.environment = updatedEnvironment;
if (updatedEvaluationContext != null) {
this.evaluationContext = updatedEvaluationContext;
} else {
logger.error(getEnvironmentUpdateErrorMessage());
}
Expand Down Expand Up @@ -150,13 +134,10 @@ public Flags getIdentityFlags(String identifier, Map<String, Object> traits)
public Flags getIdentityFlags(String identifier, Map<String, Object> traits, boolean isTransient)
throws FlagsmithClientError {
if (getShouldUseEnvironmentDocument()) {
return getIdentityFlagsFromDocument(
identifier,
ModelUtils.getTraitModelsFromTraitMap(traits));
return getIdentityFlagsFromDocument(identifier, traits);
}

return getIdentityFlagsFromApi(
identifier, ModelUtils.getSdkTraitModelsFromTraitMap(traits), isTransient);
return getIdentityFlagsFromApi(identifier, traits, isTransient);
}

/**
Expand All @@ -180,20 +161,18 @@ public List<Segment> getIdentitySegments(String identifier)
*/
public List<Segment> getIdentitySegments(String identifier, Map<String, Object> traits)
throws FlagsmithClientError {
if (environment == null) {
if (evaluationContext == null) {
throw new FlagsmithClientError("Local evaluation required to obtain identity segments.");
}
IdentityModel identityModel = getIdentityModel(
identifier,
(traits != null
? ModelUtils.getTraitModelsFromTraitMap(traits)
: new ArrayList<TraitModel>()));
List<SegmentModel> segmentModels = SegmentEvaluator.getIdentitySegments(
environment, identityModel);

return segmentModels.stream().map((segmentModel) -> {

final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext(
evaluationContext, identifier, traits);

final EvaluationResult result = Engine.getEvaluationResult(context);

return result.getSegments().stream().map((segmentModel) -> {
Segment segment = new Segment();
segment.setId(segmentModel.getId());
segment.setId(Integer.valueOf(segmentModel.getKey()));
segment.setName(segmentModel.getName());

return segment;
Expand All @@ -212,37 +191,39 @@ public void close() {
}

private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand we're trying to avoid too many changes but do you think we can benefit from making private function names consistent?

There are also other similar cases.

Suggested change
private Flags getEnvironmentFlagsFromDocument() throws FlagsmithClientError {
private Flags getEnvironmentFlagsFromLocalEvaluationContext() throws FlagsmithClientError {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestion, done in 988d59b.

if (environment == null) {
if (evaluationContext == null) {
if (getConfig().getFlagsmithFlagDefaults() == null) {
throw new FlagsmithClientError("Unable to get flags. No environment present.");
}
return getDefaultFlags();
}

return Flags.fromFeatureStateModels(
Engine.getEnvironmentFeatureStates(environment),
final EvaluationResult result = Engine.getEvaluationResult(evaluationContext);

return Flags.fromEvaluationResult(
result,
getConfig().getAnalyticsProcessor(),
null,
getConfig().getFlagsmithFlagDefaults());
}

private Flags getIdentityFlagsFromDocument(
String identifier, List<? extends TraitModel> traitModels)
String identifier, Map<String, Object> traits)
throws FlagsmithClientError {
if (environment == null) {
if (evaluationContext == null) {
if (getConfig().getFlagsmithFlagDefaults() == null) {
throw new FlagsmithClientError("Unable to get flags. No environment present.");
}
return getDefaultFlags();
}

IdentityModel identity = getIdentityModel(identifier, traitModels);
List<FeatureStateModel> featureStates = Engine.getIdentityFeatureStates(environment, identity);
final EvaluationContext context = EngineMappers.mapContextAndIdentityDataToContext(
evaluationContext, identifier, traits);

return Flags.fromFeatureStateModels(
featureStates,
final EvaluationResult result = Engine.getEvaluationResult(context);

return Flags.fromEvaluationResult(
result,
getConfig().getAnalyticsProcessor(),
identity.getCompositeKey(),
getConfig().getFlagsmithFlagDefaults());
}

Expand All @@ -252,7 +233,7 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError {
} catch (Exception e) {
if (getConfig().getFlagsmithFlagDefaults() != null) {
return getDefaultFlags();
} else if (environment != null) {
} else if (evaluationContext != null) {
try {
return getEnvironmentFlagsFromDocument();
} catch (FlagsmithClientError ce) {
Expand All @@ -265,20 +246,20 @@ private Flags getEnvironmentFlagsFromApi() throws FlagsmithApiError {
}

private Flags getIdentityFlagsFromApi(
String identifier, List<SdkTraitModel> traitModels, boolean isTransient)
String identifier, Map<String, Object> traits, boolean isTransient)
throws FlagsmithApiError {
try {
return flagsmithSdk.identifyUserWithTraits(
identifier,
traitModels,
ModelUtils.getSdkTraitModelsFromTraitMap(traits),
isTransient,
Boolean.TRUE);
} catch (Exception e) {
if (getConfig().getFlagsmithFlagDefaults() != null) {
return getDefaultFlags();
} else if (environment != null) {
} else if (evaluationContext != null) {
try {
return getIdentityFlagsFromDocument(identifier, traitModels);
return getIdentityFlagsFromDocument(identifier, traits);
} catch (FlagsmithClientError ce) {
// Do nothing and fall through to FlagsmithApiError
}
Expand All @@ -288,37 +269,14 @@ private Flags getIdentityFlagsFromApi(
}
}

private IdentityModel getIdentityModel(String identifier, List<? extends TraitModel> traitModels)
throws FlagsmithClientError {
if (environment == null) {
throw new FlagsmithClientError(
"Unable to build identity model when no local environment present.");
}

if (identitiesWithOverridesByIdentifier != null) {
IdentityModel identityOverride = identitiesWithOverridesByIdentifier.get(identifier);
if (identityOverride != null) {
identityOverride.updateTraits(traitModels);
return identityOverride;
}
}

IdentityModel identity = new IdentityModel();
identity.setIdentityTraits(traitModels);
identity.setEnvironmentApiKey(environment.getApiKey());
identity.setIdentifier(identifier);

return identity;
}

private Flags getDefaultFlags() {
Flags flags = new Flags();
flags.setDefaultFlagHandler(getConfig().getFlagsmithFlagDefaults());
return flags;
}

private String getEnvironmentUpdateErrorMessage() {
if (this.environment == null) {
if (this.evaluationContext == null) {
return "Unable to update environment from API. "
+ "No environment configured - using defaultHandler if configured.";
} else {
Expand Down Expand Up @@ -574,7 +532,7 @@ public FlagsmithClient build() {
throw new FlagsmithRuntimeError(
"Cannot use both default flag handler and offline handler.");
}
client.environment = configuration.getOfflineHandler().getEnvironment();
client.evaluationContext = configuration.getOfflineHandler().getEvaluationContext();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following our discussions, reminder to revert here

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Zaimwa9 @khvn26 Do you mind sharing the context?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was referring to dropping the offline handler of the current scope and leaving it read the environment document and return it (no breaking change on that part for users)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now done.

}

return this.client;
Expand Down
Loading