Skip to main content

One post tagged with "web"

View All Tags

· 11 min read
Pete Hodgson

While OpenFeature initially focused on support for server-side feature flagging, we know that a lot of feature-flagging (likely the majority) happens on the client - mobile apps and frontend web apps. As such, we're currently finalizing a proposal which extends the OpenFeature spec to support client-side use cases. By the way, if you're working on a feature flagging framework, whether it's commercial, open-source, or internal product, the folks at OpenFeature would love to hear more about how you approach client-side flagging.

In this post I'll summarize those changes, but to understand them in context we'll first talk about what makes client-side feature flagging different before diving into how that will impact the OpenFeature APIs.

Context is King: why client-side flagging is different

Static evaluation context

The key distinction between the client- and server-side feature flagging is the difference in how often evaluation context changes. In the typical server-side scenario - a service responding to http requests - the context for a feature flagging decision might change completely with every incoming request. Each new request is coming from a new user, and a lot of the evaluation context which affects a feature flagging decision is based on the user making the request.

In contrast, with a client-side app all feature flagging decisions are made in the context of the same user - the user interacting with the client-side app - and so the evaluation context is relatively static. There are cases where evaluation context will change within a client-side app - when a user logs in, for example - but by and large with client-side code we can treat feature flag evaluation context as something that is fixed (while still providing mechanisms to update it).

Network calls are slow

In the server, client processes might pull rule-sets for evaluating flags and use those as the basis for local flag evaluation. In other cases, flags might be evaluated out-of-process (by a remote service or some co-located process) and the results of the evaluation sent back to the client. Regardless, with server-side flags we can assume that evaluating a feature flag is a relatively fast operation, even in cases where evaluations take place out-of-process.

This situation is quite different with client-side flags. In cases of local evaluation, a ruleset (or a collection of them) must be sent to the client, in bulk or piecemeal. In cases of remote evaluation, the client must contact a remote server with the relevant context and receive an evaluated result.

This means that a client-side systems require network calls. Unfortunately we should also anticipate the latency for such a call to be slow, particularly if our users are behind a spotty internet connection. In fact, with a native mobile app we have to handle a fully disconnected client.

Locally-evaluated systems and initialization

For systems wherein flags are evaluated locally, signalling readiness to the consuming application can be of particular importance. Until the ruleset governing flag evaluation is fetched, we can't guarantee accurate flag values for the given context. We need an API that supports events or other asynchronous mechanisms to communicate the state of the underlying system.

sequenceDiagram actor app participant client as feature flagging logic participant cache participant provider as flag service app->>+client: user visits page client->>+cache: get last known state cache->>-client: . client->>+provider: get ruleset from last known state provider-->>-client: ruleset client->>+cache: Store ruleset cache->>-client: . %% deactivate cache client->>-app: . %% deactivate client note right of app: some time later.. app->>+client: operation that needs a flagging decision client->>client: flag evaluation client->>-app: locally evaluated value returned

Remotely-evaluated systems and eager evaluation

Remote evaluation presupposes network calls for flag evaluation, but we've also seen that the inputs into that flag evaluation - the evaluation context - are fairly static for client-side apps, and that means the results of flag evaluation are fairly static too.

How do we handle an expensive operation with fairly static results? We add caching! And that's what most client-side feature flagging frameworks do.

Specifically, they do an optimistic pre-evaluation of all the feature flagging decisions that might be needed and then cache those decisions. Then whenever client-side code needs to make a flagging decision the framework simply returns the pre-evaluated result from its local cache.

sequenceDiagram actor app participant client as feature flagging logic participant cache participant provider as flag service app->>+client: userLoggedIn(userId) client->>+provider: evaluateFlags(evaluationContext) provider-->>-client: flag values client->>+cache: store(flag values) cache->>-client: . %% deactivate cache client->>-app: . %% deactivate client note right of app: some time later.. app->>+client: operation that needs a flagging decision client->>+cache: getFlagValue(flagKey) note right of cache: no call to flagging service needed cache->>-client: previously evaluated value client->>-app: .

Put another way, with client-side feature flagging we can separate flag evaluation - passing an evaluation context through a set of rules in order to determine a flagging decision - from flag resolution - getting the flagging decision for a specific feature flag.

Client-side support in OpenFeature

With OpenFeature we have been thinking about how to support these key differences between client-side and server-side feature flagging. We have come to refer to these differences as two paradigms: dynamic context (for server-side flags) and static context (for client-side flags).

OpenFeature's current Evaluation API supports the dynamic paradigm quite nicely, but to support the static paradigm (and thus client-side flagging) we need to add a second flavor of the Evaluation API.

Server-side evaluation today

Let's compare and contrast. A typical server-side flagging decision using OpenFeature's current SDK might look something like this:

@GetMapping("/hello")
public String getSalutation() {
final Client client = openFeatureAPI.getClient();
final evalContext:EvaluationContext = evalContextForCurrentRequest();

if (client.getBooleanValue("use-formal-salutation", false, evalContext)) {
return "Good day to you!";
}else{
return "Hey, what's up?";
}
}

You can see that we're passing evaluation context every time we need to make a flagging decision.

Client-side evaluation tomorrow

With the currently proposed OpenFeature changes, a client-side flagging decision would look more like this:

public string generateSalutation(){
if (client.getBooleanValue("use-formal-salutation", false)) {
return "Good day to you!";
}else{
return "Hey, what's up?";
}
}

Note that we are no longer passing evaluation context when requesting a flagging decision; this happened when the SDK and provider were initialized.

However OpenFeature does still need to take evaluation context into account, and our app still needs to make sure that OpenFeature has an accurate view of the current context. What does that look like?

We can imagine a client-side app where the evaluation context only changes when a user logs in or out. Let's say this app has an onAuthenticated(...) handler which fires whenever that happens. We can use that handler to make sure that the evaluation context used for subsequent feature flagging decision is up-to-date:

// called whenever a user logs in (or out)
public void onAuthenticated(userId:String){
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setEvaluationContext(new MutableContext().add("targetingKey", userId));
}

This call to update the evaluation context can prompt OpenFeature's underlying flagging provider to update any cached feature flag values using the new evaluation context, or update its ruleset if the ruleset is context-sensitive. The provider will be notified that the evaluation context has changed via a new onContextSet handler which is being added to the OpenFeature provider interface:

class MyFlaggingProvider implements Provider {
// triggered when `setEvaluationContext` is called
onContextSet(EvaluationContext oldContext, EvaluationContext newContext): void {
// here the provider can re-evaluate flags using the next evaluation context, updating any
// previously cached flag values or updating rulesets
}
//...
}
sequenceDiagram actor user participant app participant of as OpenFeature SDK participant provider as OpenFeature Provider participant backend as provider backend user->>app: logs in app->>+app: onAuthenticated(userId) app->>of: setEvaluationContext({targetingKey:userId}) of->>+provider: onContextSet(oldContext,newContext) deactivate app provider->>+backend: evaluateFlags(newContext) backend->>-provider: flag values provider->>provider: update cache with new values deactivate provider

Javascript niceties

JavaScript is the most common runtime for client-side flagging, and it comes with some peculiarities which we wanted to accommodate as we designed the OpenFeature API.

In order to align the OpenFeature API with most existing feature flagging providers and to play nicely with frontend frameworks, the static context flavor of the JavaScript Evaluation API will be a synchronous call:

function Salutation() {
const useFormalSalutation = client.getBooleanValue('use-formal-salutation', false);
if (formalSalutation) {
return <blink>Good day!</blink>;
} else {
return <blink>What's up!</blink>;
}
}

contrast this with how a server-side flagging decision would be implemented, using the dynamic context flavor of the Evaluation API:

const client = OpenFeature.getClient();

app.get('/hello', async (req, res) => {
const evalContext = evaluationContextForRequest(req);
const formalSalutation = await client.getBooleanValue('use-formal-salutation', false, evalContext);
if (formalSalutation) {
res.send('Good day!');
} else {
res.send("What's up!");
}
});

Note that getBooleanValue is async - it returns a promise. That's pretty much unavoidable. This dynamic flavor of the evaluation API expects a different evaluation context every time it's called, and that means it can't be synchronous, because some feature flagging implementations will need to use asynchronous mechanisms such as networks calls in order to perform a flag evaluation with that new context.

However in the earlier client-side example we are in the static context paradigm which means that the evaluation context has been provided ahead of time, so we can pretty safely assume that the flagging decision can be made synchronously by pulling from a local cache.

Non-authoritative results

This synchronous API also lines up better with the the way rendering works in client-side web frameworks like React and Vue - via a synchronous call. However, with a synchronous API we have a potential for a race condition. What happens if our rendering logic asks for a flagging decision before a pre-evaluation operation has completed? We can't block, and so will have to return some sort of non-authoritative decision - a default value, or a previously evaluated value, or perhaps a null value.

client.setContext(updatedEvalContext);

// this result will NOT be based on the evaluation context we
// provided immediately above - our feature flagging framework
// won't have had a chance to actually send this new context
// anywhere
const result = client.getBooleanValue('some-flag', true);

This potential for stale or non-authoritative results is the price we pay for using a synchronous API.

When we have received a non-authoritative answer we can pass it on to our rendering logic, but we also need some way to know when an authoritative value is available, so that we can trigger a re-render using that new value. OpenFeature will provide a mechanism for that in the form of Provider Events. Here's how you might use provider events as part of a React hook:

client.addHandler(ProviderEvents.FlagValuesChanged, () => {
// this would trigger a re-render
setUseFormalSalutation(client.getBooleanValue('use-formal-salutation', false));
});

A multi-paradigm SDK

To recap, OpenFeature is planning to support client-side feature flagging by introducing the concept of two flagging paradigms: dynamic context (for server-side flagging) and static context (for client-side flagging). The OpenFeature APIs will have two slightly different flavors, depending upon which paradigm they support.

This raises a question around distribution - how will we distribute these two flavors of API for languages such as JavaScript and Java which support both paradigms. Our intention in those situations is to distribute two distinct packages - for example with JavaScript we will likely have a @openfeature/js-node-sdk package and a @openfeature/js-browser-sdk package. We are opting for this approach rather that distributing a single "universal", multi-paradigm package contain both APIs because we think this will be less confusing for developers getting started with OpenFeature. But under the covers the two OpenFeature packages will share a lot of common code.

If a feature flagging provider wants to provide support for both paradigms they will need to provide two distinct implementations of the Provider API. These implementations could be shipped as part of a "universal" multi-paradigm, or as separate packages - that's a choice for a provider to make.

In summary

We've seen that the difference between feature flagging on the client vs on the server comes down to the difference between two paradigms of evaluation context - static context and dynamic context, respectively. These two paradigms are different enough that they warrant two different flavors of API within OpenFeature, with the primary difference between whether highly-dynamic evaluation context is provided to the flagging framework at the point a flagging decision is requested, or whether mostly-static evaluation context is updated out-of-band when it changes, and ahead of the time when flagging decisions being made.

Getting better support in OpenFeature for client-side flags comes down to supporting both of these paradigms, and when you lay it all out adding that support doesn't have too huge an impact on the OpenFeature API. As we just discussed, it means adding a different flavor of the Evaluation API. In addition a few extra points of contact are added to the API surface so an app can tell the framework that the evaluation context has changed, and so the framework can in turn tell the app once new flag values are available.

With these additions in place, OpenFeature should have everything needed to bring client-side apps an open, vendor-agnostic standard for feature flagging.