Skip to content

Commit 7f7f7ee

Browse files
linear-code[bot]emyllerclaude
authored
feat(Flag Dependency): Evaluate flags that depend on other flags (#240)
Co-authored-by: linear-code[bot] <222613912+linear-code[bot]@users.noreply.github.com> Co-authored-by: Evandro Myller <22429+emyller@users.noreply.github.com> Co-authored-by: Evandro Myller <evandro.myller@flagsmith.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 3e778a5 commit 7f7f7ee

10 files changed

Lines changed: 461 additions & 126 deletions

File tree

‎.github/workflows/sdk-spec.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ permissions:
1111
jobs:
1212
sdk-spec:
1313
# Keep the ref in `uses:` and `spec-ref` in sync.
14-
uses: Flagsmith/sdk-spec/.github/workflows/run-spec.yml@v0.3.0
14+
uses: Flagsmith/sdk-spec/.github/workflows/run-spec.yml@v0.7.0
1515
with:
16-
spec-ref: v0.3.0
16+
spec-ref: v0.7.0
1717
language: java
18+
tags: core or local-evaluation

‎.gitmodules‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[submodule "src/test/java/com/flagsmith/flagengine/enginetestdata"]
22
path = src/test/java/com/flagsmith/flagengine/enginetestdata
33
url = git@github.com:Flagsmith/engine-test-data.git
4-
branch = v3.7.0
4+
branch = v3.11.0
55
[submodule "third-party/codspeed-jvm"]
66
path = third-party/codspeed-jvm
77
url = https://github.com/CodSpeedHQ/codspeed-jvm.git

‎sdk-spec/Steps.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import com.flagsmith.FlagsmithClient;
2+
import com.flagsmith.config.FlagsmithConfig;
23
import com.flagsmith.exceptions.FlagsmithClientError;
34
import com.flagsmith.models.BaseFlag;
45
import com.flagsmith.models.Flags;
@@ -35,6 +36,17 @@ static FlagsmithClient createClient(String environmentKey, String apiUrl) {
3536
return FlagsmithClient.newBuilder().setApiKey(environmentKey).withApiUrl(apiUrl).build();
3637
}
3738

39+
// Create the SDK client with local evaluation
40+
static FlagsmithClient createClientWithLocalEvaluation(String environmentKey, String apiUrl) {
41+
return FlagsmithClient.newBuilder()
42+
.setApiKey(environmentKey)
43+
.withConfiguration(FlagsmithConfig.newBuilder()
44+
.baseUri(apiUrl)
45+
.withLocalEvaluation(true)
46+
.build())
47+
.build();
48+
}
49+
3850
// When the application gets the environment flags
3951
static List<SpecFlag> getEnvironmentFlags(FlagsmithClient flagsmith)
4052
throws FlagsmithClientError {

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

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import com.flagsmith.flagengine.Engine;
1010
import com.flagsmith.flagengine.EvaluationContext;
1111
import com.flagsmith.flagengine.EvaluationResult;
12+
import com.flagsmith.flagengine.SegmentRule;
13+
import com.flagsmith.flagengine.Segments;
1214
import com.flagsmith.interfaces.FlagsmithCache;
1315
import com.flagsmith.interfaces.FlagsmithSdk;
1416
import com.flagsmith.mappers.EngineMappers;
@@ -30,9 +32,11 @@
3032
import java.util.stream.Collectors;
3133
import lombok.AccessLevel;
3234
import lombok.Data;
35+
import lombok.EqualsAndHashCode;
3336
import lombok.Getter;
3437
import lombok.NonNull;
3538
import lombok.Setter;
39+
import lombok.ToString;
3640
import org.apache.commons.lang3.StringUtils;
3741
import org.slf4j.Logger;
3842
import org.slf4j.LoggerFactory;
@@ -43,9 +47,17 @@
4347
@Data
4448
public class FlagsmithClient {
4549

50+
private static final List<String> IDENTITY_FREE_PROPERTY_PREFIXES = List.of(
51+
"$.environment.", "$.environment[", "$.flags.", "$.flags[");
52+
4653
private final FlagsmithLogger logger = new FlagsmithLogger();
4754
private FlagsmithSdk flagsmithSdk;
4855
private EvaluationContext evaluationContext;
56+
@Getter(AccessLevel.NONE)
57+
@Setter(AccessLevel.NONE)
58+
@EqualsAndHashCode.Exclude
59+
@ToString.Exclude
60+
private EvaluationContext environmentEvaluationContext;
4961
private PollingManager pollingManager;
5062
@Getter(AccessLevel.PACKAGE)
5163
@Setter(AccessLevel.NONE)
@@ -58,6 +70,19 @@ public static FlagsmithClient.Builder newBuilder() {
5870
return new FlagsmithClient.Builder();
5971
}
6072

73+
/**
74+
* Set the evaluation context, and derive the one environment flags evaluate against.
75+
*
76+
* @param evaluationContext the evaluation context
77+
*/
78+
public void setEvaluationContext(EvaluationContext evaluationContext) {
79+
this.environmentEvaluationContext = evaluationContext == null
80+
? null
81+
: new EvaluationContext(evaluationContext)
82+
.withSegments(getIdentityFreeSegments(evaluationContext.getSegments()));
83+
this.evaluationContext = evaluationContext;
84+
}
85+
6186
/**
6287
* Load the environment flags in the environment variable from the API.
6388
*/
@@ -68,7 +93,7 @@ public void updateEnvironment() {
6893
// if we didn't get an environment from the API,
6994
// then don't overwrite the copy we already have.
7095
if (updatedEvaluationContext != null) {
71-
this.evaluationContext = updatedEvaluationContext;
96+
setEvaluationContext(updatedEvaluationContext);
7297
} else {
7398
logger.error(getEnvironmentUpdateErrorMessage());
7499
}
@@ -411,24 +436,48 @@ private EventProcessor requireEventProcessor(String action) {
411436
}
412437

413438
private Flags getEnvironmentFlagsFromEvaluationContext() throws FlagsmithClientError {
414-
if (evaluationContext == null) {
439+
final EvaluationContext context = environmentEvaluationContext;
440+
if (context == null) {
415441
if (getConfig().getFlagsmithFlagDefaults() == null) {
416442
throw new FlagsmithClientError("Unable to get flags. No environment present.");
417443
}
418444
return getDefaultFlags();
419445
}
420446

421-
final EvaluationResult result = Engine.getEvaluationResult(
422-
new EvaluationContext(evaluationContext)
423-
.withSegments(null)
424-
);
447+
final EvaluationResult result = Engine.getEvaluationResult(context);
425448

426449
return Flags.fromEvaluationResult(
427450
result,
428451
getConfig().getAnalyticsProcessor(),
429452
getConfig().getFlagsmithFlagDefaults());
430453
}
431454

455+
/*
456+
* The segments that resolve without an identity, i.e. those whose every
457+
* condition reads only the environment or other flags.
458+
*/
459+
private static Segments getIdentityFreeSegments(Segments segments) {
460+
if (segments == null) {
461+
return null;
462+
}
463+
Segments identityFreeSegments = new Segments();
464+
segments.getAdditionalProperties().forEach((segmentKey, segmentContext) -> {
465+
if (isIdentityFree(segmentContext.getRules())) {
466+
identityFreeSegments.setAdditionalProperty(segmentKey, segmentContext);
467+
}
468+
});
469+
return identityFreeSegments;
470+
}
471+
472+
private static boolean isIdentityFree(List<SegmentRule> rules) {
473+
return rules == null || rules.stream().allMatch(rule ->
474+
(rule.getConditions() == null || rule.getConditions().stream().allMatch(condition ->
475+
condition.getProperty() != null
476+
&& IDENTITY_FREE_PROPERTY_PREFIXES.stream()
477+
.anyMatch(condition.getProperty()::startsWith)))
478+
&& isIdentityFree(rule.getRules()));
479+
}
480+
432481
private Flags getIdentityFlagsFromEvaluationContext(
433482
String identifier, Map<String, Object> traits)
434483
throws FlagsmithClientError {
@@ -748,8 +797,8 @@ public FlagsmithClient build() {
748797
throw new FlagsmithRuntimeError(
749798
"Cannot use both default flag handler and offline handler.");
750799
}
751-
client.evaluationContext = EngineMappers.mapEnvironmentToContext(
752-
configuration.getOfflineHandler().getEnvironment());
800+
client.setEvaluationContext(EngineMappers.mapEnvironmentToContext(
801+
configuration.getOfflineHandler().getEnvironment()));
753802
}
754803

755804
EventProcessor processor = null;

0 commit comments

Comments
 (0)