Conversation
There was a problem hiding this comment.
Pull request overview
Updates the Java OpenFeature provider to call the newer evaluations endpoint so the provider can consume updated evaluation payloads (including fields needed for client percentage rollout) without breaking older deployed clients.
Changes:
- Switched the evaluations URI to
/api/toggles/evaluations/v3/(provider + WireMock test server). - Updated
FeatureToggleEvaluationto removename, and addevaluationKey,clientRolloutPercentage, andOptional-based accessors for nullable fields. - Updated
OctopusContextevaluation flow to account for the new nullable fields and fail fast when required fields for client-side evaluation are missing.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/octopus/openfeature/provider/OctopusClient.java | Calls the new evaluations endpoint and updates Jackson TypeReference usage. |
| src/main/java/com/octopus/openfeature/provider/FeatureToggleEvaluation.java | Adjusts evaluation model to match the new endpoint (new fields + Optional getters). |
| src/main/java/com/octopus/openfeature/provider/OctopusContext.java | Adds validation for required client-side evaluation properties; updates segment evaluation to use Optional segments. |
| src/test/java/com/octopus/openfeature/provider/Server.java | Updates WireMock stubbed path to the new evaluations endpoint. |
| src/test/java/com/octopus/openfeature/provider/OctopusContextTests.java | Updates test data shape and adds ParseError coverage for missing required evaluation fields. |
| src/test/java/com/octopus/openfeature/provider/FeatureToggleEvaluationDeserializationTests.java | Updates deserialization assertions for the new model shape (Optional segments, removed name). |
Comments suppressed due to low confidence (1)
src/test/java/com/octopus/openfeature/provider/FeatureToggleEvaluationDeserializationTests.java:34
- The model now includes
evaluationKeyandclientRolloutPercentage, but the deserialization tests don't assert their presence/absence for enabled vs disabled toggles. Add assertions (and update fixtures if needed) to ensure these fields are correctly deserialized into the newOptionalgetters, so regressions in the new endpoint shape are caught.
@Test
void shouldDeserializeEnabledToggle() throws Exception {
FeatureToggleEvaluation result = objectMapper.readValue(resource("toggle-enabled-no-segments.json"), FeatureToggleEvaluation.class);
assertThat(result.getSlug()).isEqualTo("my-feature");
assertThat(result.isEnabled()).isTrue();
assertThat(result.getSegments()).isEmpty();
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public String getName() { | ||
| return name; | ||
| this.evaluationKey = evaluationKey; | ||
| this.segments = segments; |
There was a problem hiding this comment.
segments is assigned directly from the constructor parameter and later wrapped with Collections.unmodifiableList(...) in the getter. This still allows external mutation if the original list reference is retained (e.g., callers constructing FeatureToggleEvaluation directly), which can lead to hard-to-trace behavior changes. Consider making a defensive copy in the constructor (e.g., List.copyOf(segments) when non-null) so the instance is truly immutable.
| this.segments = segments; | |
| this.segments = segments == null ? null : List.copyOf(segments); |
There was a problem hiding this comment.
Low risk, but seems good to me.
src/test/java/com/octopus/openfeature/provider/FeatureToggleEvaluationDeserializationTests.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!evaluation.isEnabled()) { | ||
| return false; | ||
| } | ||
|
|
||
| return evaluation.getClientRolloutPercentage().isEmpty() | ||
| || evaluation.getEvaluationKey().isEmpty() | ||
| || evaluation.getSegments().isEmpty(); |
There was a problem hiding this comment.
missingRequiredPropertiesForClientSideEvaluation causes evaluate(...) to throw ParseError for any enabled toggle where segments is missing, even though evaluate can safely treat missing segments as "no targeting" (and you already have a fixture covering a missing segments field). This changes behavior from returning the server-provided isEnabled value to falling back to the caller’s default. Consider treating a missing segments field as an empty list (or only requiring segments when client-side targeting/rollout evaluation is actually performed).
| if (!evaluation.isEnabled()) { | |
| return false; | |
| } | |
| return evaluation.getClientRolloutPercentage().isEmpty() | |
| || evaluation.getEvaluationKey().isEmpty() | |
| || evaluation.getSegments().isEmpty(); | |
| // Client-side evaluation is only applicable when the toggle is enabled and has segments | |
| if (!evaluation.isEnabled() || !evaluation.hasSegments()) { | |
| return false; | |
| } | |
| return evaluation.getClientRolloutPercentage().isEmpty() | |
| || evaluation.getEvaluationKey().isEmpty(); |
There was a problem hiding this comment.
I think this might be correct. Will give it more thought tomorrow.
There was a problem hiding this comment.
This matches what the .NET client is doing and passes the missing-client-validation-fields specification tests.
| if (missingRequiredPropertiesForClientSideEvaluation(toggleValue)) { | ||
| throw new ParseError("Feature toggle " + toggleValue.getSlug() + " is missing necessary information for client-side evaluation."); | ||
| } |
There was a problem hiding this comment.
The ParseError message is generic and doesn’t indicate which required fields are missing. Including the missing property names (e.g., evaluationKey/segments/clientRolloutPercentage) would make diagnosing malformed responses much easier, especially since the exception causes OpenFeature to fall back to the default value.
| @ParameterizedTest(name = "[{0}] {1}") | ||
| @MethodSource("fixtureTestCases") | ||
| @Disabled("Requires either old endpoint to be used, or client rollout percentage to be implemented") | ||
| void evaluate(String fileName, String description, String responseJson, FixtureCase testCase) { |
There was a problem hiding this comment.
This test suite is now fully disabled. That removes the only end-to-end coverage against the provider specification fixtures and makes it easier for future changes to regress behavior unnoticed. Consider marking these as a separate tagged/optional suite (e.g., via JUnit @Tag + build profile) or splitting out the cases that still apply so some specification coverage continues to run in CI.
There was a problem hiding this comment.
Haven't had these all this time, I guess it's fine to disable them for a bit.
There was a problem hiding this comment.
Temporary circumstance as per PR description.
| import java.io.InputStream; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; |
There was a problem hiding this comment.
Unused import java.util.Optional is present but not referenced anywhere in this test class. Removing it will keep the file clean and avoid warnings in stricter build setups.
| import java.util.Optional; |
| // this exception will be handled by OpenFeature, and the default value will be used | ||
| if (toggleValue == null) { | ||
| throw new FlagNotFoundError(); | ||
| throw new FlagNotFoundError(); |
There was a problem hiding this comment.
Interesting. Java implementation throws rather than returning an error response?
There was a problem hiding this comment.
The specification test seems to confirm this for the parse error case.
There was a problem hiding this comment.
Of note: these exception types also exist in the .NET SDK: https://github.com/open-feature/dotnet-sdk/tree/main/src/OpenFeature/Error
We may want to align the clients in one direction or the other at some point.
caitlynstocker
left a comment
There was a problem hiding this comment.
Have picked through the code and can't see any problems ✅
I'm not sure exactly how to do it but have you also tested it manually?
|
|
||
| public Optional<List<Segment>> getSegments() { | ||
| return Optional.ofNullable(segments); | ||
| } |
There was a problem hiding this comment.
This is my first time seeing this Optional class, but it sounds right 👍
| return null; | ||
| } | ||
| List<FeatureToggleEvaluation> evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<List<FeatureToggleEvaluation>>(){}); | ||
| List<FeatureToggleEvaluation> evaluations = OctopusObjectMapper.INSTANCE.readValue(httpResponse.body(), new TypeReference<>(){}); |
There was a problem hiding this comment.
Have not seen this before but Claude says you're just telling the deserializer to put the response into a list of FeatureToggleEvaluations. Sounds good 👍
There was a problem hiding this comment.
Yeah, this is just a bit of syntax clean up. Because the type of the variable List<FeatureToggleEvaluation> is specified (no var in Java 8), it isn't needed to be specified in the TypeReference<>.
|
|
||
| assertThat(result.getSegments()).hasSize(2); | ||
| assertSegmentsContain(result.getSegments(), | ||
| var segments = result.getSegments().orElseThrow(); |
There was a problem hiding this comment.
If we throw an error here, is it possible to include a message about why?
There was a problem hiding this comment.
I could add an explicit check and throw my own exception, but I think it should be clear if it were ever to throw at this line? You can basically think of .orElseThrow() as the same as C#'s !, except it throws right away rather than when you try to reference the variable.
I have tested against my local OctoToggle service: package com.octopus.openfeature.provider;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.OpenFeatureAPI;
import org.junit.jupiter.api.Test;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
class OpenFeatureIntegrationTest {
@Test
void shouldInitialiseOpenFeatureClientWithOctopusProvider() {
var config = new OctopusConfiguration("eyJh...");
config.setServerUri(URI.create("http://localhost:7133"));
var provider = new OctopusProvider(config);
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProviderAndWait(provider);
Client client = api.getClient();
assertThat(client).isNotNull();
assertThat(api.getProviderMetadata().getName()).isEqualTo("octopus");
assertThat(client.getBooleanValue("alpha-03", false)).isTrue();
}
} |
Background
The client percentage rollout feature requires
ClientRolloutPercentageandEvaluationKeyto be included in evaluation responses.We were unable to add these fields to the existing
GET /featuretoggles/v3evaluations endpoint because the Java OpenFeature provider (in this repo) was previously using Jackson with strict deserialisation (FAIL_ON_UNKNOWN_PROPERTIES = true). Even though we relaxed this in #5, we still can't change the existing endpoint because there are deployed versions of the older serialisation configuration.This PR updates the provider to use the new endpoint, which has the following changes compared to the old endpoint:
namefield was removedevaluationKeyandclientRolloutPercentagewere added, though are only returned ifisEnabledevaluated totruein the feature toggles servicesegmentsare also only returned ifisEnabledevaluated totrueChanges
FeatureToggleEvaluationclassnameproperty and getterevaluationKeyandclientRolloutPercentageproperties withOptionalgettersgetSegmentstoOptionalgetterhasSegmentsconvenience methodOctopusContextclassFeatureToggleEvaluationmissingRequiredPropertiesForClientSideEvaluationcheckNotes for review
null) we can use theOptional<T>type to make it explicit which getter values should be checked or defaulted.Testing
OctopusDeploy/openfeature-provider-specificationthat uses the new endpoint response shape that doesn't also expect the client percentage rollout feature to be implemented. This will be implemented and fully tested in the next PR, prior to the next release of this library