Releases: canopy-labs/featureflip-java
Release list
v2.10.0
Fixed
- The polling fallback no longer ends the SSE stream. After five consecutive failures — about 31 seconds of unreachability, so an edge incident, a bad deploy or a network partition —
SseDataSourcehanded over to a polling fallback and scheduled no further reconnect, and nothing else re-opened the stream: every affected JVM lost real-time updates until the process restarted, and polled/v1/sdk/flagsevery 30 seconds indefinitely. Flag changes, kill switches included, then arrived up to a poll interval late. Polling is now additive — it covers the outage while the stream keeps retrying underneath at the capped, jittered backoff, and the next deliveredsyncretires the poller. Retiring it matters as much as arming it: a poller left running beside a healthy stream reverts SSE deltas with its own whole-store replaces, and a second outage falls back again. (#3071)
v2.9.0
Added
-
FeatureflipClient.onUpdate(FlagUpdateListener)— subscribe to flag-configuration changes. The listener is called with the flag keys whose evaluated value may have moved, batched into one call per update, and the returned action unsubscribes (idempotently; subscriptions are also dropped when the client is closed). Brings Java level with the JS, Python and Go SDKs, and is what lets the OpenFeature provider emitPROVIDER_CONFIGURATION_CHANGED. (#1867)The reported set is deliberately wider than "flags whose own config changed", because two kinds of movement leave no trace on the flag that moved. A segment edit changes what a rule matches without touching — or versioning — any flag that references it. A prerequisite toggle bumps only the prerequisite's version, while every dependent flips to its off variation with
PREREQUISITE_FAILED. Both are followed, the second transitively, so a listener that re-reads what it is told about sees every value that actually changed.The initial flag load does not fire — a cold start is not a change — and neither does a snapshot identical to the one held. That second point is load-bearing rather than an optimization: the store is handed a full snapshot on every poll tick and on every SSE reconnect, so without the comparison a listener would fire once per poll interval forever. Comparison is structural over the serialized configuration rather than a version check, since a segment edit moves a flag's value without moving its version.
-
FeatureflipClient.jsonVariationDetail(key, context, defaultValue, type)— the detail counterpart ofjsonVariation. Until now JSON was the one flag type whose evaluation reason, rule id and served variation key were unreachable: every other type had a*VariationDetailtwin and JSON did not.Passing
Object.classreads a value without asserting its type, yielding the plain Java shapes Jackson produces (Map,List,String,Integer,Double,Boolean). That form cannot fail as a type mismatch, which is what lets a caller — the OpenFeature provider being the first — distinguish a genuine evaluation error from a value of the wrong type. The typed accessors fold both intoERROR.
Fixed
- The README's quickstart did not compile. It reached for
EvaluationContext.of(...), a factory this SDK has never had, in three of its examples — including the very first one — so anyone copying the getting-started snippet hitcannot find symbolbefore they hit anything else. All three now useEvaluationContext.builder(userId).build(), which is what the API actually offers.
v2.8.0
Added
-
EvaluationContext.builder()— a no-arg builder producing an anonymous context: attributes, no identity. Until nowbuilder(userId)was the only constructor and it rejected a null id, so a context carrying attributes with no identity could not be built at all — a shape every other SDK expresses trivially. Analytics events from such a context omituserIdrather than sending an empty string. (#2665)The workaround this replaces was actively wrong on the wire. A caller wanting an anonymous event with attributes had two spellings and both were broken:
identify(null)omitteduserIdcorrectly but a null context carries no attributes, whileidentify(builder("").set(…))carried the attributes but emitted"userId": ""— a present-but-empty identity, the exact shape #2397 removed from the PHP SDK. Ordinary evaluation pushed callers toward the broken one, sincebuilder("")is how a keyless context was spelled: evaluation was always correct (an empty bucket value serves the control variation, #1457), so a caller doing the supported thing for anonymous evaluation silently emitted non-conforming events.builder("")is unchanged and still carries a present-but-empty identity. That is deliberate: the JS, Python, PHP and Ruby SDKs all attribute events off a null identity rather than an empty one, so an explicit""reaches the wire in four of the five map-context SDKs; only Go drops it, and by accident ofomitemptyrather than by an absent-vs-empty decision. Folding""into "absent" here would have silently changed the wire shape for existing callers to match the one SDK that never decided it.
v2.7.0
Changed
- A long-lived SSE stream severed mid-frame is now logged at
DEBUGinstead ofWARN. Such a sever is routine behind a CDN or reverse proxy: the client reconnects and the server replays a fullsyncsnapshot, so no configuration is missed and nothing is degraded — but reporting it atWARNturned ordinary operation into a recurring alarm. A stream that never opened is still logged atWARN, and the fallback-to-polling warning is unchanged, so a genuinely broken stream stays visible. (#2457)
Fixed
-
An explicit
client.flush()no longer opens a second drain loop while one is already running. The in-flight latch added for #2456 guarded only the batch-size trigger, so the periodic flush, an explicitclient.flush()and a size-triggered flush could enter the loop together — two request streams against an endpoint the backoff gate exists to protect, and worse, a success in one cleared the gate a failure in the other had just armed, re-opening the one-request-per-evaluation behaviour outright. A caller arriving while a drain is running now waits for it and returns, matching the js and node SDKs. Shutdown still bypasses coalescing, because it is the last drain there will ever be. (#2477) -
A
Before/Afterdate operand that resolves outside the representable date range now matches nothing, where it previously resolved to a real instant. The evaluation engine parses withDateTimeOffset.TryParse, so its accepted range is 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999Z and it matches nothing outside that; this SDK resolved past both ends, so a single saved rule served different variations to two users purely by which SDK their service ran. (#2500) -
The two reachable shapes are a year-zero operand and an operand carried out of range by its offset.
0000-01-01is inside the ISO grammar and is a real proleptic date (0000-02-29exists — year 0 is divisible by 400), so neither #2480's grammar guard nor #2491's calendar-day check excluded it. Separately,[0-9]{4}constrains only the written year while a timezone offset moves the resolved instant, so0001-01-01T00:00:00+05:00fell below the floor and9999-12-31T23:59:59-05:00rose above the ceiling from years the grammar allows. The check therefore runs on the resolved instant — deliberately unlike #2491's, which runs on the written date. (#2500) -
The exact boundaries remain accepted:
0001-01-01,0001-01-01T05:00:00+05:00,9999-12-31T23:59:59Zand9999-12-31T18:59:59-05:00all still resolve. (#2500)
If you have a targeting rule using one of these operands, rewrite it as the date you meant. The Management API has rejected them on write since #2480 (PortableDateOperand round-trips every grammar-matched operand through the engine's own parser, so it inherits the range bound), meaning only rules saved before that release can carry one.
- The first SSE reconnect after a healthy stream drops is now jittered to
[d/2, d], like every other backoff level. The drops this absorbs are fleet-wide — a single edge event severs every stream at once — so every client re-entered the backoff together and waited an identical delay, republishing the drop's own synchronisation as a reconnect spike one backoff later. Measured in production: a drop spread across 2.5–3.0 ms produced a reconnect spread of 26–46 ms. The delay never exceeds the previous one and stays strictly positive, so a stream that fails immediately still cannot busy-loop. (#2508)