spec icon indicating copy to clipboard operation
spec copied to clipboard

Subscribe to variable updates

Open bencehornak opened this issue 2 months ago • 8 comments

Feature request

When working with DevCycle I have come to notice that OpenFeature doesn't have a convenient way for subscribing to updates of a certain variable similar to DevCycle's variable updates:

var variable: Variable<String> = devcycleClient.variable("str_key", "default")
variable.onUpdate {
    // grab the variable value using it.value
}

Source code

The Events API provides generic callbacks only currently

What the Events spec provides is a global mechanism for listening to updates in general:

The data that providers supply in event payloads may include a list of flag keys changed, error messages, and possibly updated flag values. [See the Event details data type]

Kotlin example:

var myFlag = OpenFeatureAPI.getClient().getBooleanValue("myflag", ...)
OpenFeatureAPI.observe().onEach { event ->
  if(event.eventDetails?.flagsChanged.contains("myflag")) {
    myFlag = OpenFeatureAPI.getClient().getBooleanValue("myflag", ...)
}

So as you can see it's quite a bit of boilerplate for just tracking the values of one variable. And you have to ask fetch the new value again, it's not part of the event payload, making this idiom less convenient.

Motivation

As an application developer I would like to activate/deactivate features of my long-running client-side apps instantaneously. My app is not restarted for days or weeks, so I'd like to subscribe to variable changes in my application code.

Proposal

Some reactive API could be added to the Flag Evaluation API, which is convenient to use as an app developer, such as:

class MyClass {
  private var subscription: ...

  fun init() {
    subscription = OpenFeatureAPI.getClient().onBooleanValueChanges("myflag", "default", { oldValue, newValue ->
      // do some stuff with newValue
      updateUI(newValue)
    })
  }

  fun close() {
    subscription.close()
  }
}

P.S.: If you like the suggestion, feel free to pick up this topic, I won't have much time in the coming weeks for contribution.

bencehornak avatar Oct 19 '25 13:10 bencehornak

Hey @bencehornak, thanks for kicking this off. I like the idea. Not only does it help users discover that subscriptions are possible, but it's also more developer-friendly to use.

Based on your proposal, I have a few questions. Would onBooleanValueChanges only return the value? If so, would it be possible to get the full evaluation details somehow? Also, would the callback fire only when the value changes or when any attribute changes (e.g. REASON)? I believe in the React SDK we only re-render on value change.

Another consideration is that not every provider emits a change event or includes a list of changed flags. We'll need to mention in the docs that events are provider specific. In the React SDK, we re-render if the flag is included in the list of changed flags or the list is null/undefined. I believe that behavior makes sense in this case as well.

beeme1mr avatar Oct 21 '25 13:10 beeme1mr

Hey @bencehornak, Jonathan from DevCycle here. I'm happy to work with you to make this happen. It's one of the last major differences between our DevCycle SDKs and the OpenFeature SDKs.

To quickly answer how it works in DevCycle, this onUpdate callback gets fired when the value changes (though it could make sense if the reason changes too). I agree with @bencehornak that the callback attached to the DevCycle Variable can really help reduce the observe boilerplate code.

However, I think it might be more useful to add the onValueUpdated callback to the getBooleanDetails / EvaluationDetails class:

class MyClass {
  private var subscription: ...

  fun init() {
    var myFlagDetails = OpenFeatureAPI.getClient().onBooleanDetails("myflag", "default");
    myFlagDetails.onValueUpdated({ oldValue, newValue ->
      // do some stuff with newValue
      updateUI(newValue)
    })
  }

  fun close() {
    subscription.close()
  }
}

jonathannorris avatar Oct 21 '25 15:10 jonathannorris

Kotlin example:

var myFlag = OpenFeatureAPI.getClient().getBooleanValue("myflag", ...) OpenFeatureAPI.observe().onEach { event -> if(event.eventDetails?.flagsChanged.contains("myflag")) { myFlag = OpenFeatureAPI.getClient().getBooleanValue("myflag", ...) }

The existing events API is designed the way it is for 2 reasons:

  • As @beeme1mr mentioned, not all providers support events at all, and of those that do, not all of them support events that supply the flags that were changed, or provide APIs for subscribing to changes on a particular flag.
  • Forcing the user to re-evaluate the flag again in the "changed" callback (instead of directly providing the new value) forces the same, well-understood flag evaluation life-cycle again (ie: the hooks run in the expected order, the context used is the one supplied in the evaluation, etc). If we we to provide the flag value in a callback directly, we'd have to carefully specify what context is used and consider the impacts of if it's been mutated since the callback was setup, what hooks are used, etc, which might get a bit messy.

The goal of the OpenFeature SDK is to prevent a single API covering a sort of "union" of common functionality with some advanced features on top, and I'm a bit concerned this functionality can't be consistently implemented across most providers and also in concert with our other APIs (at least, that was our initial conclusion when we first implemented the events API).

toddbaert avatar Oct 21 '25 15:10 toddbaert

  • If we we to provide the flag value in a callback directly, we'd have to carefully specify what context is used and consider the impacts of if it's been mutated since the callback was setup, what hooks are used, etc, which might get a bit messy.

That's a good point. I forgot about that aspect. It may still be worth considering since not all use cases require a fully dynamic context. It would be trivial to introduce something like this in the OpenFeature CLI. Perhaps that's an approach worth considering.

beeme1mr avatar Oct 21 '25 15:10 beeme1mr

Yea good points @toddbaert / @beeme1mr I see the hook lifecycle probably being the biggest blocker here.

From my context, I'm thinking about this in only the client-side / static-context SDKs (we have only implemented it in our client SDKs), which simplifies the complexities of dynamic context on the server-side SDKs.

Thinking through this, there may be a path forward for client-side / static-context SDKs only without relying on events. You'd need to change the callback handler to be something like onContextChanged so that it re-evaluates the hooks for that flag on every context change:

-> static context changed -> flags with onContextChanged callbacks are evaluated again through the normal evaluation flow with hooks -> oldValue / newValue is returned to all onContextChanged callbacks for each flag

You are telling the OpenFeature SDK to re-evaluate this flag on every context change, which makes sense for many UI-based flows on client SDKs. If that's the case, maybe it would make more sense for this to be more of a top-level API for client SDKs that implements this behaviour closer to what @bencehornak originally proposed.

jonathannorris avatar Oct 21 '25 17:10 jonathannorris

If the normal evaluation path is executed for all changes, and the provider determines what a change is (which may include no visible change), then I wouldn't have a large concern with this for client-side.

My primary concern with per-flag event handlers is ensuring that proper analytics can still be generated. There are sometimes changes that aren't visible from the raw value. For instance going from a disabled state of a flag, to the control variation for an experiment involving that flag. The value may be the same, but some meta-data may be different.

kinyoklion avatar Oct 21 '25 17:10 kinyoklion

Another consideration is that not every provider emits a change event or includes a list of changed flags. We'll need to mention in the docs that events are provider specific.

@beeme1mr But this is already the case. For providers without events, app authors would not realize that the provider is missing events these when subscribing. For the React and Angular SDK we have this too. But my feeling is, that app authors would be aware of the features of the used provider or realize that they overlooked it latest when they are missing the re-render.

If we we to provide the flag value in a callback directly, we'd have to carefully specify what context is used [...]

I think for the dynamic context SDKs (server), this would be too complex for the amount of boilerplate we save. As @toddbaert says, if users have the option to react to changed flags like it is today, they are aware of re-evaluating and the possibility that the flag value for the context did not change, while being aware of the context used. For the static context (web), this should be okay as the context will be the same anyways. I think actually, this is the same as what we do for the React and Angular SDKs. [React Hook] [Angular Directive] [Angular Service]

Thinking through this, there may be a path forward for client-side / static-context SDKs only without relying on events. You'd need to change the callback handler to be something like onContextChanged so that it re-evaluates the hooks for that flag on every context change:

As said above, if I get you right @jonathannorris this is exactly what we do in these three examples for the Angular and React SDKs: [React Hook] [Angular Directive] [Angular Service]

You are telling the OpenFeature SDK to re-evaluate this flag on every context change, which makes sense for many UI-based flows on client SDKs. If that's the case, maybe it would make more sense for this to be more of a top-level API for client SDKs that implements this behaviour closer to what @bencehornak originally proposed.

Yes, I think so too, we have that implementation duplicated in React and Angular already.

My primary concern with per-flag event handlers is ensuring that proper analytics can still be generated. There are sometimes changes that aren't visible from the raw value. For instance going from a disabled state of a flag, to the control variation for an experiment involving that flag. The value may be the same, but some meta-data may be different.

@kinyoklion What is the exact concern here? The current implementations for Angular and React would re-evaluate the flag if flagsChanged is not attached to the event or if it contains the flag. This would then depend on how and when the provider and underlying ff system emit this event. Some might emit the event for all flags if any rule changed or a flag was enabled, some might only do it if the specific flag changed in the rules or even only if the rules changed and the flag with the current session context evaluates to a different value. Either case would e.g. trigger an evaluation, but some would trigger way less evaluations for unrelated flags. But we have this problem with event based re-evaluations already today.

lukas-reining avatar Oct 22 '25 20:10 lukas-reining

Outlined the two potential approaches here with the JS Web SDK: https://github.com/open-feature/js-sdk/pull/1281

jonathannorris avatar Nov 05 '25 22:11 jonathannorris