featureflip

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 6, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

Featureflip OpenFeature Provider (Go)

OpenFeature provider backed by the Featureflip Go server SDK. Evaluate Featureflip flags through the vendor-neutral OpenFeature API, so swapping flag vendors is a provider registration rather than a rewrite of every call site.

Installation

go get github.com/canopy-labs/featureflip-go-openfeature

The OpenFeature SDK and the Featureflip SDK are both direct dependencies:

go get github.com/open-feature/go-sdk
go get github.com/canopy-labs/featureflip-go/v2

Quickstart

package main

import (
	"context"
	"log"

	featureflip "github.com/canopy-labs/featureflip-go-openfeature"
	"github.com/open-feature/go-sdk/openfeature"
)

func main() {
	// SetProviderAndWait, not SetProvider: the plain form initializes on a
	// background goroutine and returns before the first flag load has
	// finished, so evaluations on the next lines hand back their defaults.
	if err := openfeature.SetProviderAndWait(featureflip.NewProvider("sdk-your-key")); err != nil {
		log.Fatal(err)
	}
	defer openfeature.Shutdown()

	client := openfeature.NewClient("my-app")

	enabled, err := client.BooleanValue(
		context.Background(),
		"new-checkout",
		false,
		openfeature.NewEvaluationContext("user-123", map[string]any{
			"plan":    "pro",
			"country": "NZ",
		}),
	)
	if err != nil {
		log.Printf("evaluation error: %v", err)
	}
	log.Printf("new-checkout: %v", enabled)
}

If you import the Featureflip SDK directly as well, alias one of them — both packages are named featureflip:

import (
	sdk "github.com/canopy-labs/featureflip-go/v2"
	featureflip "github.com/canopy-labs/featureflip-go-openfeature"
)

Sharing a client

Pass an existing client when the same process also evaluates flags through the SDK directly, so both paths share one client rather than opening a second stream. The caller keeps ownership: Shutdown will not close it.

client, err := sdk.Get("sdk-your-key")
if err != nil {
	log.Fatal(err)
}
defer client.Close()

if err := openfeature.SetProviderAndWait(featureflip.NewProviderWithClient(client)); err != nil {
	log.Fatal(err)
}

Configuration

NewProvider forwards SDK options:

featureflip.NewProvider("sdk-your-key",
	sdk.WithInitTimeout(10*time.Second),
	sdk.WithStreaming(true),
)

Context mapping

The OpenFeature targeting key becomes the Featureflip user id, which is what percentage rollouts bucket on. Every other attribute is passed through unchanged, so a targeting rule written against plan or country in the dashboard reads the attribute of that name.

The Featureflip SDK resolves the attribute names userId and user_id from its dedicated user-id field rather than from the attribute map, so all three spellings are lifted into it. An explicit user_id or userId wins over targetingKey, on the grounds that a caller who set both meant the explicit one.

openfeature.NewEvaluationContext("user-123", map[string]any{"plan": "pro"})
// -> UserID: "user-123", Attributes: {"plan": "pro"}

Evaluation reasons

Featureflip OpenFeature
RuleMatch TARGETING_MATCH
Fallthrough DEFAULT
FlagDisabled DISABLED
PrerequisiteFailed PREREQUISITE_FAILED
FlagNotFound ERROR (FLAG_NOT_FOUND)
Error ERROR (GENERAL)

PREREQUISITE_FAILED is not a standard OpenFeature reason. Reasons are open strings, and surfacing it verbatim is more useful than mislabelling it DEFAULT or DISABLED. An unmet prerequisite still serves the flag's off variation, so the value is real and correctly typed.

ruleId and prerequisiteKey are exposed through flag metadata when the evaluation carries them.

Type safety

A read whose flag value does not match the requested type resolves to your default with TYPE_MISMATCH, rather than returning the wrong-typed value.

Integer reads accept whole-number JSON values in either form (1 and 1.0), because JSON does not distinguish them and a flag's readability should not depend on how its value happened to be serialized. Object reads accept objects and arrays only; a string is rejected, since a caller reaching for an object wants structure.

Tracking

The provider implements OpenFeature's Tracker, forwarding to the SDK's analytics events:

client.Track(context.Background(), "checkout-completed",
	openfeature.NewEvaluationContext("user-123", nil),
	openfeature.NewTrackingEventDetails(99.90).Add("currency", "USD"))

Reacting to flag changes

The provider emits OpenFeature's PROVIDER_CONFIGURATION_CHANGED whenever flag configuration changes:

callback := func(details openfeature.EventDetails) {
	log.Printf("flags changed: %v", details.FlagChanges)
}
openfeature.AddHandler(openfeature.ProviderConfigChange, &callback)

FlagChanges covers more than the flags whose own rows moved. Editing a segment changes evaluated outcomes without bumping any flag's version, so the flags referencing it are included; and a flag depending on a changed flag through a prerequisite is included too, because its value really does flip while its own configuration is untouched.

The initial load is not reported — OpenFeature signals that with PROVIDER_READY — and neither is a refresh that changed nothing. The SDK re-fetches the whole configuration on every poll tick and every stream reconnect, so reporting those would mean an event per interval rather than an event per change.

Events are delivered on a buffered channel. If nothing drains it, further events are dropped and logged rather than blocking the SDK goroutine that delivers flags.

Limitations

  • Reading a flag before the provider is initialized resolves to your default with PROVIDER_NOT_READY.

Documentation

License

Apache-2.0

Documentation

Overview

Package featureflip provides an OpenFeature provider backed by the Featureflip Go server SDK.

Register it with the OpenFeature API and evaluate flags through the standard client:

provider := featureflip.NewProvider("sdk-xxx")
if err := openfeature.SetProviderAndWait(provider); err != nil {
	log.Fatal(err)
}
client := openfeature.NewClient("my-app")
enabled := client.Boolean(ctx, "new-checkout", false, openfeature.NewEvaluationContext(
	"user-123", map[string]any{"plan": "pro"},
))

Use SetProviderAndWait rather than SetProvider: the plain form runs Init on a background goroutine and returns before the first flag load has finished, so evaluations on the following lines can hand back their defaults.

If you also import the Featureflip SDK directly, alias one of the two — both packages are named featureflip:

import (
	sdk "github.com/canopy-labs/featureflip-go/v2"
	featureflip "github.com/canopy-labs/featureflip-go-openfeature"
)

Index

Constants

View Source
const PrerequisiteFailedReason openfeature.Reason = "PREREQUISITE_FAILED"

PrerequisiteFailedReason is returned when a flag short-circuited because one of its prerequisites did not serve its expected variation.

No standard OpenFeature reason models an unmet prerequisite. Reasons are open strings, so this surfaces it verbatim rather than mislabelling it as DEFAULT or DISABLED — the same choice the Node, .NET and Python providers made.

Variables

This section is empty.

Functions

This section is empty.

Types

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is an OpenFeature provider backed by the Featureflip Go server SDK.

Construct it either with an SDK key, in which case the provider creates and owns the underlying client, or with an existing client, in which case the caller keeps ownership and Shutdown leaves it open.

A Provider is safe for concurrent use.

func NewProvider

func NewProvider(sdkKey string, opts ...sdk.Option) *Provider

NewProvider returns a provider that owns its client.

The client is constructed in Init, not here, because construction blocks on the initial flag fetch. Building it in the constructor would move that wait out of SetProviderAndWait, where callers expect it.

If sdkKey is empty the SDK falls back to the FEATUREFLIP_SDK_KEY environment variable, and Init reports an error if neither is set.

func NewProviderWithClient

func NewProviderWithClient(client *sdk.Client) *Provider

NewProviderWithClient returns a provider backed by an existing client.

The caller retains ownership: Shutdown will not close it. Use this when the same process also evaluates flags through the SDK directly, so that both paths share one client rather than opening a second stream.

func (*Provider) BooleanEvaluation

func (p *Provider) BooleanEvaluation(
	_ context.Context, flag string, defaultValue bool, flatCtx openfeature.FlattenedContext,
) openfeature.BoolResolutionDetail

BooleanEvaluation resolves a boolean flag.

func (*Provider) EventChannel added in v0.2.0

func (p *Provider) EventChannel() <-chan openfeature.Event

EventChannel exposes this provider's event stream to the OpenFeature SDK.

The channel is never closed. The SDK's reader treats a close as "stop listening", but closing it here would race any listener still in flight and panic on a send to a closed channel; the SDK exits that goroutine on its own shutdown signal instead.

func (*Provider) FloatEvaluation

func (p *Provider) FloatEvaluation(
	_ context.Context, flag string, defaultValue float64, flatCtx openfeature.FlattenedContext,
) openfeature.FloatResolutionDetail

FloatEvaluation resolves a float flag.

func (*Provider) Hooks

func (p *Provider) Hooks() []openfeature.Hook

Hooks returns no provider hooks; Featureflip has nothing to add here.

func (*Provider) Init

Init constructs the client if this provider owns one.

The evaluation context is unused: Featureflip is a dynamic-context (server) provider, so targeting comes from the per-evaluation context rather than being bound at initialization.

It is safe to call more than once; OpenFeature may re-initialize a provider that is already initialized.

func (*Provider) IntEvaluation

func (p *Provider) IntEvaluation(
	_ context.Context, flag string, defaultValue int64, flatCtx openfeature.FlattenedContext,
) openfeature.IntResolutionDetail

IntEvaluation resolves an integer flag.

func (*Provider) Metadata

func (p *Provider) Metadata() openfeature.Metadata

Metadata identifies this provider to the OpenFeature SDK.

func (*Provider) ObjectEvaluation

func (p *Provider) ObjectEvaluation(
	_ context.Context, flag string, defaultValue any, flatCtx openfeature.FlattenedContext,
) openfeature.InterfaceResolutionDetail

ObjectEvaluation resolves an object or array flag.

func (*Provider) Shutdown

func (p *Provider) Shutdown()

Shutdown closes the client only if this provider created it.

A caller-supplied client is a refcounted handle the caller still holds; closing it would make THEIR handle start returning defaults.

func (*Provider) StringEvaluation

func (p *Provider) StringEvaluation(
	_ context.Context, flag string, defaultValue string, flatCtx openfeature.FlattenedContext,
) openfeature.StringResolutionDetail

StringEvaluation resolves a string flag.

func (*Provider) Track

func (p *Provider) Track(
	_ context.Context,
	trackingEventName string,
	evaluationContext openfeature.EvaluationContext,
	details openfeature.TrackingEventDetails,
)

Track records a custom analytics event against the Featureflip client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL