Skip to content

Releases: canopy-labs/featureflip-java

v2.10.0

Choose a tag to compare

@jhoward321 jhoward321 released this 18 Sep 20:33

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 — SseDataSource handed 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/flags every 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 delivered sync retires 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

Choose a tag to compare

@jhoward321 jhoward321 released this 07 Sep 00:27

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 emit PROVIDER_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 of jsonVariation. Until now JSON was the one flag type whose evaluation reason, rule id and served variation key were unreachable: every other type had a *VariationDetail twin and JSON did not.

    Passing Object.class reads 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 into ERROR.

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 hit cannot find symbol before they hit anything else. All three now use EvaluationContext.builder(userId).build(), which is what the API actually offers.

v2.8.0

Choose a tag to compare

@jhoward321 jhoward321 released this 07 Sep 00:26

Added

  • EvaluationContext.builder() — a no-arg builder producing an anonymous context: attributes, no identity. Until now builder(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 omit userId rather 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) omitted userId correctly but a null context carries no attributes, while identify(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, since builder("") 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 of omitempty rather 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

Choose a tag to compare

@jhoward321 jhoward321 released this 27 Aug 18:20

Changed

  • A long-lived SSE stream severed mid-frame is now logged at DEBUG instead of WARN. Such a sever is routine behind a CDN or reverse proxy: the client reconnects and the server replays a full sync snapshot, so no configuration is missed and nothing is degraded — but reporting it at WARN turned ordinary operation into a recurring alarm. A stream that never opened is still logged at WARN, 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 explicit client.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/After date operand that resolves outside the representable date range now matches nothing, where it previously resolved to a real instant. The evaluation engine parses with DateTimeOffset.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-01 is inside the ISO grammar and is a real proleptic date (0000-02-29 exists — 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, so 0001-01-01T00:00:00+05:00 fell below the floor and 9999-12-31T23:59:59-05:00 rose 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:59Z and 9999-12-31T18:59:59-05:00 all 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)