OpenFeature and OpenTelemetry

Use featflags through the OpenFeature API, and record every flag evaluation on your OpenTelemetry traces.

OpenFeature

OpenFeature is a vendor-neutral API for feature flags. Code written against it reads featflags flags once you register the featflags provider, so you can move to featflags, or away from it, without touching the call sites. Ten SDKs ship a provider:

SDK Provider Package
Go ffopenfeature.NewProvider github.com/featflags/featflags-go/openfeature
Python featflags.openfeature.FeatflagsProvider featflags[openfeature] on PyPI
Java io.featflags.openfeature.FeatflagsProvider io.featflags:featflags-openfeature
.NET FeatFlags.OpenFeature.FeatFlagsProvider FeatFlags.OpenFeature on NuGet
Ruby Featflags::OpenFeature::Provider the featflags-openfeature gem
PHP Featflags\OpenFeature\Provider featflags/openfeature-provider on Packagist
Elixir Featflags.OpenFeature.Provider featflags_openfeature on Hex
iOS FeatFlagsProvider the FeatFlagsOpenFeature library of the Swift package
Android FeatflagsProvider io.featflags.kotlin:featflags-openfeature
Flutter FeatFlagsOpenFeatureProvider featflags_openfeature on pub.dev

The TypeScript, React, React Native and Rust SDKs have no OpenFeature provider; use their own API.

In every SDK the provider wraps a featflags client you create and keep: it doesn’t own the client, so close or dispose it yourself when your app stops.

Registering the provider

On a server, register the provider once and pass an evaluation context with each call:

openfeature.SetProviderAndWait(ffopenfeature.NewProvider(flags))
client := openfeature.NewClient("checkout")
on, _ := client.BooleanValue(ctx, "new-checkout", false, openfeature.NewEvaluationContext("u-alice", map[string]any{"plan": "pro"}))
api.set_provider(FeatflagsProvider(flags))
on = api.get_client("checkout").get_boolean_value("new-checkout", False, EvaluationContext("u-alice", {"plan": "pro"}))
OpenFeatureAPI.getInstance().setProviderAndWait(new FeatflagsProvider(flags));
boolean on = OpenFeatureAPI.getInstance().getClient("checkout")
        .getBooleanValue("new-checkout", false, new ImmutableContext("u-alice", Map.of("plan", new Value("pro"))));
await Api.Instance.SetProviderAsync(new FeatFlagsProvider(flags));
var on = await Api.Instance.GetClient("checkout")
    .GetBooleanValueAsync("new-checkout", false, EvaluationContext.Builder().SetTargetingKey("u-alice").Set("plan", "pro").Build());
OpenFeature::SDK.configure { |config| config.set_provider(Featflags::OpenFeature::Provider.new(flags)) }
on = OpenFeature::SDK.build_client.fetch_boolean_value(flag_key: "new-checkout", default_value: false,
       evaluation_context: OpenFeature::SDK::EvaluationContext.new(targeting_key: "u-alice", plan: "pro"))
$api = OpenFeatureAPI::getInstance();
$api->setProvider(new Featflags\OpenFeature\Provider($flags));
$on = $api->getClient()->getBooleanValue('new-checkout', false, new EvaluationContext('u-alice', new Attributes(['plan' => 'pro'])));
{:ok, _} = OpenFeature.set_provider(Featflags.OpenFeature.Provider.new(MyApp.Flags))
OpenFeature.Client.get_boolean_value(OpenFeature.get_client(), "new-checkout", false, context: %{targeting_key: "u-alice", plan: "pro"})

In apps, OpenFeature’s client SDKs hold one context at a time, as featflags’s client SDKs do: setting the evaluation context identifies the user.

await OpenFeatureAPI.shared.setProviderAndWait(
    provider: FeatFlagsProvider(client: flags),
    initialContext: ImmutableContext(targetingKey: user.id, structure: ImmutableStructure(attributes: ["plan": .string("pro")])))
OpenFeatureAPI.shared.getClient().getBooleanValue(key: "new-checkout", defaultValue: false)
OpenFeatureAPI.setProviderAndWait(FeatflagsProvider(flags), initialContext = ImmutableContext(targetingKey = userId))
OpenFeatureAPI.getClient().getBooleanValue("new-checkout", false)
OpenFeatureAPI.setEvaluationContextAndWait(ImmutableContext(targetingKey = otherUserId)) // identifies the new user
await OpenFeatureAPI.instance.setEvaluationContextAndWait(EvaluationContext(targetingKey: user.id, attributes: {'plan': 'pro'}));
await OpenFeatureAPI.instance.setProviderAndWait(FeatFlagsOpenFeatureProvider(flags));
OpenFeatureAPI.instance.getClient().getBooleanValue('new-checkout', false);

Evaluation contexts

Every provider turns an OpenFeature evaluation context into a featflags context the same way:

  • The targeting key is the context key (a key attribute works too). In the iOS, Android and Flutter providers, a context without one is the anonymous user.
  • Without a kind attribute, the context is a user. kind set to another kind, such as org, makes it a single context of that kind.
  • kind: "multi" makes a multi-context: each member goes under its kind, as a nested structure with its own targetingKey. Top-level attributes that aren’t structures are ignored.
{ "kind": "multi", "user": { "targetingKey": "u-alice", "plan": "pro" }, "org": { "targetingKey": "acme", "plan": "team" } }

On servers, date-time attributes count as missing: pass instants as RFC 3339 strings or epoch milliseconds. The iOS and Flutter providers turn dates into epoch milliseconds for you. A server context without a key is TARGETING_KEY_MISSING, and one featflags rejects is INVALID_CONTEXT.

Reasons

featflags OpenFeature
target, rule TARGETING_MATCH
default, fallback DEFAULT
disabled DISABLED
prerequisite_failed PREREQUISITE_FAILED
membership_pending MEMBERSHIP_PENDING
not_found ERROR with FLAG_NOT_FOUND, or PROVIDER_NOT_READY before the first copy of your flags arrives
type_mismatch ERROR with TYPE_MISMATCH
error ERROR with GENERAL

Flag metadata carries the featflags reason, plus rule_id and prerequisite when set, except in PHP, whose OpenFeature SDK has no flag metadata: use the client’s …Detail methods there when you need them. Integer evaluations serve whole numbers that fit the type, object evaluations serve arrays and objects, and anything else is a type mismatch. Evaluation reasons explains what each featflags reason means.

The app providers (iOS, Android, Flutter) also tell OpenFeature when a newer copy of your flags may have changed evaluations, with a configuration-changed event, and the iOS and Flutter ones emit READY once the client has flags.

OpenTelemetry

Every SDK but Flutter has an OpenTelemetry hook. Register it like any hook and each evaluation inside a span adds a feature_flag.evaluation event to it, following OpenTelemetry’s semantic conventions for feature flags. Evaluations outside a span record nothing.

SDK Hook Package
TypeScript, React, React Native otelHook() from @featflags/js/otel @featflags/js, with @opentelemetry/api installed
Go ffotel.Hook{} github.com/featflags/featflags-go/otel
Python featflags.otel.OtelHook() featflags[otel]
Java io.featflags.otel.OtelHook io.featflags:featflags-otel
Rust featflags::otel::OtelHook featflags with the otel feature
.NET FeatFlags.OtelHook FeatFlags (records on Activity.Current)
Ruby Featflags::Otel::Hook the featflags-otel gem
PHP Featflags\Otel\OtelHook featflags/otel
Elixir Featflags.Otel.Hook featflags_otel
iOS OtelHook the FeatFlagsOtel library of the Swift package
Android io.featflags.kotlin.otel.OtelHook io.featflags.kotlin:featflags-otel

Flutter has none yet: OpenTelemetry has no official Dart API.

The hook lives outside the core package in every SDK but .NET, whose spans are part of .NET itself, so the SDKs stay free of dependencies you don’t use. In Go, the hook finds the span in the context.Context you pass to the evaluation.

import { otelHook } from '@featflags/js/otel'

const flags = new FlagClient({ key: 'sk_…', cdnUrl: 'https://flags.featflags.io', hooks: [otelHook()] })

The event’s attributes

Attribute Value
feature_flag.key The flag key.
feature_flag.provider.name featflags
feature_flag.result.variant The variant key; absent when your fallback was returned.
feature_flag.result.reason targeting_match (target, rule), default (default, fallback), disabled, error (not_found, type_mismatch, error), and featflags’s own prerequisite_failed and membership_pending, as the OpenFeature providers map them.
error.type flag_not_found, type_mismatch or general, for error reasons only.