Skip to content

feat!: evaluate feature flags through the v4 endpoints - #53

Open
lourens-octopus wants to merge 10 commits into
mainfrom
lourens/bmbb-753
Open

feat!: evaluate feature flags through the v4 endpoints#53
lourens-octopus wants to merge 10 commits into
mainfrom
lourens/bmbb-753

Conversation

@lourens-octopus

@lourens-octopus lourens-octopus commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Resolves BMBB-753 and BMBB-749. Java counterpart of openfeature-provider-dotnet#96, brought up to date with .NET's current code.

What

The provider now evaluates through the v4 feature-flag endpoints, using the rules-based evaluation from #51 instead of v3's segment and rollout matching.

  • api/feature-flags/check/v4/ and api/feature-flags/evaluations/v4/
  • EvaluationResponse replaces FeatureToggles; getServerSideEvaluations() replaces getFeatureToggleEvaluationManifest()
  • evaluate(slug, context) drops defaultValue and resolves a ServerSideEvaluation, so value and reason come from the server or from the rules it deferred, and a malformed response reports the problem it found
  • specification submodule ef8450967890d8

Two breaking changes

Both carry a BREAKING CHANGE: footer, so release-please cuts the major. Cutting the release is BMBB-772.

  • The endpoints — a server serving only the v3 toggle endpoints is no longer supported.
  • The reasons — no longer OpenFeature's Reason values. A server-resolved flag reports the service's own reason, a client-side match reports Matched rule '<name>'., no match reports Did not match any rules. Anything branching on Reason.TARGETING_MATCH changes silently, with no compile error.

The one decision worth your attention

refactor: flatten the v4 package into the provider package is a pure move, green on its own, and the rest of the branch sits on top of it.

The switch needs the evaluator, in the provider package, to call ServerSideEvaluation.evaluate(). Package access is not hierarchical, so a v4 sub-package would mean making that type and EvaluationResponse public — permanently, since OctopusProvider keeps its fully-qualified name and so the parent→v4 call outlives BMBB-780. With v4 now the only contract, the sub-package has served its purpose. Everything stays package-private, the public API is unchanged, and #51's duplicated rollout hash collapses back to one implementation.

The five fix: commits after the switch each came from a review of this branch, and each carries its own test — null entries and null bodies no longer cost the whole response, a null flag key answers FLAG_NOT_FOUND as v3 did, a ParseError logs its slug, and warn-once is provider-scoped, bounded and locale-independent.

One defect deliberately left in

A property of the wrong JSON type anywhere in the response still fails the whole manifest: it breaks while the response is being read, so it never reaches the per-flag reporting that contains a missing property. Tracked as BMBB-788 and deferred, so .NET and Java stay in step rather than this branch becoming the one place it is fixed — hence the revert: commit. The fix, with tests, is parked on #54.

Scope

BMBB-749 is here because it cannot be split off: the specification bump replaces its fixtures and a submodule has one pointer, so bumping first leaves v4 fixtures against a v3 provider — 80 failures, which I hit while separating the commits — and switching first leaves v4 code against deleted fixtures. .NET adopted the fixtures inside its own switch PR for the same reason.

Left to the tickets that own them, mirroring .NET's #96#99: BMBB-780 for the unused v3 types, still present and marked TODO(BMBB-780), and BMBB-755 for .NET's #97 renames (BMBB-715 looks like a duplicate of it).

Tests

461 pass, 0 failures, including all 83 specification tests (up from 41 — the v4 fixture set is larger) and MalformedEvaluationTests at 23 shapes. Public API confirmed unchanged via javap — still OctopusProvider, OctopusConfiguration and ProductMetadata.

🤖 Generated with Claude Code

@lourens-octopus
lourens-octopus requested a review from a team as a code owner August 11, 2026 02:28
Base automatically changed from lourens/bmbb-751 to main August 11, 2026 21:00
lourens-octopus and others added 2 commits August 12, 2026 09:01
The switch to v4 needs the evaluator, which lives in the provider
package, to call ServerSideEvaluation.evaluate(). Java package access is
not hierarchical, so keeping the v4 types in a sub-package would mean
making them public — permanently, since nothing later removes the split.

With v4 becoming the only contract, the sub-package has served its
purpose: it kept an unreleased contract out of the way while v3 was live.
Everything stays package-private in one package, and the public API is
unchanged.

A pure move: no behaviour changes, and the two test-only shims that only
existed to cross the package boundary are gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Switches the provider from the v3 toggle endpoints to the v4 feature-flag
endpoints, and from v3's segment and rollout matching to the rules-based
evaluation added earlier.

- GET api/feature-flags/check/v4/ and api/feature-flags/evaluations/v4/
- EvaluationResponse replaces FeatureToggles
- evaluate(slug, context) resolves a ServerSideEvaluation, so the value
  and reason come from the server or from the client-side rules it
  deferred, and a malformed response reports the problem it found
- an unrecognised slug throws FlagNotFoundError with the message the
  other provider libraries use
- specification fixtures move to the v4 evaluations set

v3's types remain, unused, for BMBB-780.

BREAKING CHANGE: the provider now calls the v4 feature-flag endpoints and
evaluates rules-based flags. A server that only serves the v3 toggle
endpoints is no longer supported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lourens-octopus and others added 8 commits August 12, 2026 09:29
A null array entry made findEvaluationBySlug throw NullPointerException,
which is not an OpenFeatureError, so every flag in the response fell back
to the caller's default with ErrorCode.GENERAL until the content hash
changed. A null entry carries no slug and can never be the flag being
asked for, so it is now skipped.

A null response body was worse: it produced a context whose content hash
looked valid, so the check endpoint reported no change and the provider
never recovered. It is now treated as a failed fetch, like every other
unusable response.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deserializing the array in one call aborted on the first wrongly-typed
field anywhere in it, so "percentage": "lots" on one flag left every flag
in the response falling back to the caller's default. Missing fields were
already contained to the flag that carried them, because they are reported
when that flag is evaluated; a field of the wrong type never gets that far,
because it fails while the response is being read.

An evaluation that cannot be read is left out and its slug logged, so it
resolves as not found rather than being guessed at, and the rest of the
response is unaffected.

Note this is stricter than the .NET provider, which still deserializes the
array in one call and so retains the original behaviour. Worth raising
there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ordering the slug comparison so a null slug in the payload is tolerated
moved the null-intolerance onto the caller's flag key, which is the one that
actually arrives null — an unset configuration value, say. v3 answered
FLAG_NOT_FOUND; a NullPointerException is not an OpenFeatureError, so the
SDK reported ErrorCode.GENERAL with a raw exception message instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ParseError messages are shared verbatim with the other provider
libraries, so they name the problem but not the flag — v3's message did name
it, and the tests deleted with the v3 path asserted exactly that. Rather
than diverge from the shared messages, the slug is logged beside the
problem, which is what makes a malformed response traceable when a server
rollout affects several flags at once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The set of already-warned slugs lived on the evaluator, which is replaced
whenever the response changes, so it forgot every slug about once per cache
interval and warned repeatedly for a single typo — the opposite of what it
was for. It now belongs to the cache, which outlives the evaluators it
builds.

It is also bounded. Slugs come from the caller, who may derive them (one per
tenant, say), and an unbounded set would have held one string per distinct
unknown slug for the life of the process.

Keys are lowercased with Locale.ROOT rather than the default locale: under a
Turkish locale "MY-FLAG-I" lowercases to a dotless "my-flag-ı", which would
not match "my-flag-i" and so would warn twice for one slug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reasons are no longer OpenFeature's Reason values, which v3 returned; a
caller branching on Reason.TARGETING_MATCH or keying metrics on the reason
sees free-text sentences instead, with a cardinality that grows with the
number of rule names. Nothing about that surfaces as a compile error, so it
belongs in the breaking-change note rather than only in the code.

BREAKING CHANGE: evaluation reasons are no longer OpenFeature's Reason
values. A server-resolved flag reports the service's own reason, a
client-side match reports "Matched rule '<name>'." and a flag that matched
no rule reports "Did not match any rules."

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An evaluation that could not be read was left out of the response, so the
flag resolved as not found. Every other malformed shape in the shared
specification resolves with PARSE_ERROR against the caller's default, and
a flag the server did send should not look like one it did not, so the flag
is now kept and reports a parse error of its own.

This is also what a specification fixture for a wrongly-typed field would
expect, if one is added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the per-entry parsing so Java and .NET behave identically: a field
of the wrong type anywhere in the response fails the whole manifest in both.
That is a real defect in both libraries, tracked as BMBB-788 and deferred
rather than fixed here, so this branch is not the one place it gets fixed.

The null-body guard stays, along with its test: that was a separate finding
and is not part of what BMBB-788 covers.

Reverts 29901b3 and 5d7f28f. The fix is preserved on lourens/bmbb-788.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant