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
keyattribute works too). In the iOS, Android and Flutter providers, a context without one is the anonymous user. - Without a
kindattribute, the context is a user.kindset to another kind, such asorg, 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 owntargetingKey. 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. |