Documentation
¶
Overview ¶
Package featureflags provides feature toggles and gradual rollouts for SaaS applications, evaluated in memory so that a request never waits on a database.
Model ¶
A Flag has a set of named Variants (a boolean flag is the two-variant case VariantOn / VariantOff), a DefaultVariant and an Enabled kill switch. Evaluating an enabled flag for an EvalContext (user ID, tenant ID and attributes) checks, in order:
- Targets: explicit user or tenant IDs (ReasonTargetMatch).
- Rules: attribute Conditions (eq, neq, in, not_in, contains, lt, lte, gt, gte), serving a fixed variant or a weighted Distribution (ReasonRuleMatch, with the rule's index).
- Rollout: a flag-level Distribution for everyone else (ReasonRollout).
- DefaultVariant (ReasonDefault).
A disabled flag serves DefaultVariant with ReasonDisabled, and a key the snapshot does not contain yields ReasonFlagNotFound with the zero Variant.
Distributions use deterministic, sticky bucketing: see Bucket for the documented hash, which is stable across processes, platforms and releases.
Evaluation ¶
A Client holds an immutable Snapshot behind an atomic pointer. Client.Run refreshes it by polling the Source and, when a Notifier is configured, on pushed change signals (pgxstore.Listener uses PostgreSQL LISTEN/NOTIFY). Evaluation reads only the snapshot, never blocks and never panics on missing flags or a broken source: missing flags are "off" (Enabled reports false) and a failing refresh keeps the last good snapshot.
Management ¶
A Manager validates definitions, writes them to a Store with optimistic concurrency (Flag.Version; a stale write fails with ErrVersionConflict) and emits ChangeEvents for audit. The memstore and pgxstore subpackages implement Store; storetest is the conformance suite for other implementations; ParseJSON and FileSource support config-as-code; and featureflagstest gives application tests a Client they control.
Example ¶
package main
import (
"context"
"fmt"
featureflags "github.com/byteminus/go-featureflags"
"github.com/byteminus/go-featureflags/memstore"
)
func main() {
ctx := context.Background()
store := memstore.New()
manager, err := featureflags.NewManager(store, featureflags.ManagerOptions{})
if err != nil {
panic(err)
}
flag := featureflags.NewBoolFlag("new-checkout", "Redesigned checkout flow")
flag.Targets = []featureflags.Target{{Variant: featureflags.VariantOn, Users: []string{"qa-lead"}}}
flag.Rules = []featureflags.Rule{{
Conditions: []featureflags.Condition{{Attribute: "plan", Op: featureflags.OpEquals, Value: "enterprise"}},
Variant: featureflags.VariantOn,
}}
if _, err := manager.Create(ctx, flag); err != nil {
panic(err)
}
client, err := featureflags.New(store, featureflags.Options{})
if err != nil {
panic(err)
}
if err := client.Refresh(ctx); err != nil {
panic(err)
}
for _, ec := range []featureflags.EvalContext{
{UserID: "qa-lead"},
{UserID: "u-17", Attributes: map[string]any{"plan": "enterprise"}},
{UserID: "u-18", Attributes: map[string]any{"plan": "free"}},
} {
reqCtx := featureflags.WithEvalContext(ctx, ec)
r := client.Evaluate(reqCtx, "new-checkout")
fmt.Println(ec.UserID, client.Enabled(reqCtx, "new-checkout"), r.Reason, r.Index)
}
fmt.Println(client.Enabled(ctx, "no-such-flag"))
}
Output: qa-lead true target_match 0 u-17 true rule_match 0 u-18 false default -1 false
Index ¶
- Constants
- Variables
- func ActorFrom(ctx context.Context) string
- func Bucket(flagKey, salt, bucketingKey string) int
- func Validate(f Flag) error
- func ValidateKey(key string) error
- func WithActor(ctx context.Context, actor string) context.Context
- func WithEvalContext(ctx context.Context, ec EvalContext) context.Context
- type BucketBy
- type ChangeEvent
- type ChangeType
- type Client
- func (c *Client) Enabled(ctx context.Context, key string) bool
- func (c *Client) Evaluate(ctx context.Context, key string) Result
- func (c *Client) LastRefresh() time.Time
- func (c *Client) Refresh(ctx context.Context) error
- func (c *Client) Run(ctx context.Context) error
- func (c *Client) Snapshot() *Snapshot
- func (c *Client) Variant(ctx context.Context, key string) Variant
- type Condition
- type Distribution
- type EvalContext
- type EvaluationEvent
- type Flag
- type Manager
- func (m *Manager) Create(ctx context.Context, f Flag) (Flag, error)
- func (m *Manager) Delete(ctx context.Context, key string, version int64) error
- func (m *Manager) Get(ctx context.Context, key string) (Flag, error)
- func (m *Manager) List(ctx context.Context) ([]Flag, error)
- func (m *Manager) Update(ctx context.Context, f Flag) (Flag, error)
- type ManagerOptions
- type Notifier
- type Operator
- type Options
- type Reason
- type Result
- type Rule
- type Snapshot
- type Source
- type SourceFunc
- type Store
- type Target
- type ValidationError
- type Variant
- type Weight
Examples ¶
Constants ¶
const ( DefaultPollInterval = 30 * time.Second DefaultRefreshTimeout = 10 * time.Second )
Defaults applied by New for zero Options fields.
const ( VariantOn = "on" VariantOff = "off" )
Variant names used by boolean flags. Client.Enabled reports true exactly when a flag resolves to the variant named VariantOn.
const TotalWeight = 10000
TotalWeight is the sum that the weights of a Distribution must reach. Weights are expressed in basis points, so 1 is 0.01% of bucketing keys.
Variables ¶
var ( // ErrFlagNotFound reports that no flag exists with the requested key. ErrFlagNotFound = errors.New("featureflags: flag not found") // ErrFlagExists reports a Create for a key that is already taken. ErrFlagExists = errors.New("featureflags: flag already exists") // ErrVersionConflict reports a write whose expected version is not the // stored version: someone else changed the flag since it was read. ErrVersionConflict = errors.New("featureflags: version conflict") // ErrInvalidFlag reports a definition that fails validation. The // concrete error is a *ValidationError when it comes from Validate. ErrInvalidFlag = errors.New("featureflags: invalid flag") // ErrNoPayload reports Variant.Decode on a variant without a payload. ErrNoPayload = errors.New("featureflags: variant has no payload") // ErrAlreadyRunning reports a second concurrent call to Client.Run. ErrAlreadyRunning = errors.New("featureflags: client is already running") )
Sentinel errors. Stores and the Manager wrap them, so check with errors.Is.
Functions ¶
func Bucket ¶
Bucket returns the bucket in [0, TotalWeight) that bucketingKey falls into for the given flag key and salt.
The algorithm is part of the package's compatibility promise, because changing it would move users between variants of running rollouts:
input = flagKey + 0x00 + salt + 0x00 + bucketingKey (bytes as given) digest = SHA-256(input) bucket = uint64(big-endian digest[0:8]) mod 10000
Flag keys and salts cannot contain NUL bytes, so the input is unambiguous. SHA-256 is used for its uniformity and its availability in every language, which lets other services reproduce assignments; it is not a security boundary. The modulo bias of 2^64 mod 10000 is below 1e-15 and irrelevant.
Example ¶
package main
import (
"fmt"
featureflags "github.com/byteminus/go-featureflags"
)
func main() {
fmt.Println(featureflags.Bucket("new-checkout", "", "user-1"))
fmt.Println(featureflags.Bucket("new-checkout", "", "user-2"))
}
Output: 1479 8982
func Validate ¶
Validate checks a flag definition: a well-formed key and variant names, unique variants, valid JSON payloads, references to existing variants only, well-formed conditions and distributions whose weights sum to TotalWeight. The error is a *ValidationError listing every problem.
Example ¶
package main
import (
"fmt"
featureflags "github.com/byteminus/go-featureflags"
)
func main() {
flag := featureflags.NewBoolFlag("checkout", "")
flag.Rollout = &featureflags.Distribution{Weights: []featureflags.Weight{{Variant: "on", Weight: 4000}}}
fmt.Println(featureflags.Validate(flag))
}
Output: featureflags: invalid flag "checkout": rollout: weights sum to 4000, want 10000
func ValidateKey ¶
ValidateKey reports whether key is a well-formed flag key: 1 to 128 characters from a-z, 0-9, '.', '_' and '-', starting with a letter or digit. The restriction keeps keys safe in URLs, logs and the bucketing hash input. The error wraps ErrInvalidFlag.
func WithActor ¶
WithActor returns a copy of ctx carrying the identity of whoever performs management writes; it is recorded in ChangeEvent.Actor for audit.
func WithEvalContext ¶
func WithEvalContext(ctx context.Context, ec EvalContext) context.Context
WithEvalContext returns a copy of ctx carrying ec, typically set once per request by middleware after authentication.
Types ¶
type BucketBy ¶
type BucketBy string
BucketBy selects which identifier of the EvalContext feeds the bucketing hash of a Distribution.
type ChangeEvent ¶
type ChangeEvent struct {
Type ChangeType
Key string
// Actor comes from WithActor on the write's context.
Actor string
// Before is nil for ChangeCreated; After is nil for ChangeDeleted.
Before *Flag
After *Flag
At time.Time
}
ChangeEvent describes one successful management write, for audit logs.
type ChangeType ¶
type ChangeType string
ChangeType names the kind of write in a ChangeEvent.
const ( ChangeCreated ChangeType = "created" ChangeUpdated ChangeType = "updated" ChangeDeleted ChangeType = "deleted" )
Change types.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client evaluates flags against an in-memory Snapshot that is replaced atomically on every refresh, so evaluation never waits on the source.
Safe defaults: until the first successful refresh, and for any key the source does not define, evaluation reports ReasonFlagNotFound, Enabled reports false and Variant returns the zero Variant. A failing source leaves the last good snapshot in place. A Client is safe for concurrent use.
func New ¶
New returns a Client reading from source. It performs no I/O: call Refresh to load synchronously (for example to fail fast at startup) and Run to keep the snapshot current.
func (*Client) Enabled ¶
Enabled reports whether key resolves to VariantOn for the EvalContext carried by ctx. It is false for missing flags.
func (*Client) Evaluate ¶
Evaluate resolves key for the EvalContext carried by ctx and reports the variant together with the reason it was chosen.
func (*Client) LastRefresh ¶
LastRefresh returns when the snapshot was last replaced, or the zero time if no refresh has succeeded.
func (*Client) Refresh ¶
Refresh loads all flags from the source and atomically replaces the snapshot. If the source fails, the current snapshot is kept and the error is returned (and passed to OnError).
A definition that fails validation does not break the refresh: the previous version of that flag is kept if there was one, otherwise the flag is left out, and the problem is passed to OnError. An invalid row therefore never turns a working flag off.
func (*Client) Run ¶
Run keeps the snapshot current until ctx is done: it refreshes at once, then on every poll tick and on every Notifier signal, coalescing signals that arrive during a refresh. Refresh errors go to OnError and never stop the loop. Run returns nil after ctx is done and its notifier has stopped, or ErrAlreadyRunning if another Run is active.
type Condition ¶
type Condition struct {
Attribute string `json:"attribute"`
Op Operator `json:"op"`
Value any `json:"value,omitempty"`
Values []any `json:"values,omitempty"`
}
Condition compares one attribute of the EvalContext. OpIn and OpNotIn use Values; every other operator uses Value. Values must be strings, booleans or finite numbers; the ordering operators require numbers.
type Distribution ¶
type Distribution struct {
// BucketBy defaults to BucketByUser when empty. Contexts without the
// chosen identifier are not bucketed.
BucketBy BucketBy `json:"bucketBy,omitempty"`
// Salt changes the bucket of every context when changed, which reshuffles
// a rollout. At most 128 bytes, without NUL bytes.
Salt string `json:"salt,omitempty"`
// Weights must name existing variants, at most once each, with
// non-negative weights summing to TotalWeight.
Weights []Weight `json:"weights"`
}
Distribution splits contexts across variants by weight. Each context lands in a bucket in [0, TotalWeight) computed by Bucket from the flag key, Salt and the bucketing key; weights claim consecutive bucket ranges in list order.
Because the ranges are consecutive, raising the weight of the first variant only moves contexts into it: list the variant whose share grows (usually VariantOn) first and a rollout from 10% to 20% keeps the original 10%.
func PercentageRollout ¶
func PercentageRollout(basisPoints int) *Distribution
PercentageRollout returns a Distribution that serves VariantOn to the given share of users, in basis points (2500 is 25%), and VariantOff to the rest. Values outside [0, TotalWeight] produce a distribution that fails validation.
Example ¶
package main
import (
"fmt"
featureflags "github.com/byteminus/go-featureflags"
)
func main() {
flag := featureflags.NewBoolFlag("new-checkout", "")
flag.Rollout = featureflags.PercentageRollout(2500) // 25% of users
snapshot, err := featureflags.NewSnapshot([]featureflags.Flag{flag})
if err != nil {
panic(err)
}
// user-1 is in bucket 1479, below 2500; user-2 is in bucket 8982.
for _, id := range []string{"user-1", "user-2"} {
r := snapshot.Evaluate("new-checkout", featureflags.EvalContext{UserID: id})
fmt.Println(id, r.Variant.Name, r.Reason)
}
}
Output: user-1 on rollout user-2 off rollout
type EvalContext ¶
type EvalContext struct {
// UserID feeds user targets and user bucketing.
UserID string
// TenantID feeds tenant targets and tenant bucketing.
TenantID string
// Attributes feed rule conditions. Values are typically strings,
// booleans, numbers or slices of those. The map is read, never written,
// by evaluation; do not modify it after placing it in a context.
Attributes map[string]any
}
EvalContext describes who a flag is evaluated for. All fields are optional: targeting and bucketing by an empty ID never match, and conditions on an absent attribute never hold.
func EvalContextFrom ¶
func EvalContextFrom(ctx context.Context) EvalContext
EvalContextFrom returns the EvalContext carried by ctx, or the zero EvalContext when there is none.
type EvaluationEvent ¶
type EvaluationEvent struct {
Result Result
Context EvalContext
}
EvaluationEvent is passed to Options.OnEvaluation after every Client evaluation, for analytics and exposure logging.
type Flag ¶
type Flag struct {
// Key identifies the flag: 1 to 128 characters from a-z, 0-9, '.', '_'
// and '-', starting with a letter or digit.
Key string `json:"key"`
// Description is free text for the people who manage the flag.
Description string `json:"description,omitempty"`
// Enabled is the kill switch. When false, rules are ignored and every
// evaluation serves DefaultVariant with ReasonDisabled.
Enabled bool `json:"enabled"`
// Variants lists the values the flag can serve. Names are unique.
Variants []Variant `json:"variants"`
// DefaultVariant names the variant served when the flag is disabled or
// when no target, rule or rollout chooses a variant.
DefaultVariant string `json:"defaultVariant"`
// Targets serve a fixed variant to listed users or tenants. They are
// checked in order before any rule.
Targets []Target `json:"targets,omitempty"`
// Rules are checked in order after Targets; the first rule whose
// conditions all hold and that serves a variant wins.
Rules []Rule `json:"rules,omitempty"`
// Rollout, when set, distributes every context that no target or rule
// claimed across variants by deterministic bucketing.
Rollout *Distribution `json:"rollout,omitempty"`
// Version is the optimistic concurrency token assigned by the Store. It
// starts at 1 and grows by one on every update.
Version int64 `json:"-"`
// UpdatedAt is the UTC time of the last write, assigned by the Store.
UpdatedAt time.Time `json:"-"`
}
Flag is the complete definition of one feature flag.
Evaluation of an enabled flag walks Targets, then Rules, then Rollout, and serves the first variant chosen; when nothing chooses one it serves DefaultVariant. A disabled flag (the kill switch) always serves DefaultVariant, so the default should be the safe, "feature off" variant.
Version and UpdatedAt are maintained by the Store and are not part of the JSON definition.
Example (Variants) ¶
package main
import (
"fmt"
featureflags "github.com/byteminus/go-featureflags"
)
func main() {
flag := featureflags.Flag{
Key: "pricing-page",
Enabled: true,
Variants: []featureflags.Variant{
{Name: "control"},
{Name: "annual-first", Payload: []byte(`{"highlight":"annual"}`)},
},
DefaultVariant: "control",
Rollout: &featureflags.Distribution{
BucketBy: featureflags.BucketByTenant,
Weights: []featureflags.Weight{
{Variant: "control", Weight: 5000},
{Variant: "annual-first", Weight: 5000},
},
},
}
snapshot, err := featureflags.NewSnapshot([]featureflags.Flag{flag})
if err != nil {
panic(err)
}
// Every user of a tenant sees the same arm, and the arm never changes.
a := snapshot.Evaluate("pricing-page", featureflags.EvalContext{UserID: "u1", TenantID: "tenant-42"})
b := snapshot.Evaluate("pricing-page", featureflags.EvalContext{UserID: "u2", TenantID: "tenant-42"})
fmt.Println(a.Variant.Name == b.Variant.Name)
}
Output: true
func NewBoolFlag ¶
NewBoolFlag returns an enabled boolean flag with variants VariantOn and VariantOff that serves VariantOff until targets, rules or a rollout are added.
func ParseJSON ¶
ParseJSON reads a config-as-code document of the form {"flags": [...]}, where each element is a flag definition in the JSON form of Flag. Decoding is strict: unknown fields, trailing data, invalid definitions and duplicate keys are errors, so a typo fails the deploy instead of silently changing behaviour. Every flag of the document is validated and all problems are reported together.
Example ¶
package main
import (
"fmt"
"strings"
featureflags "github.com/byteminus/go-featureflags"
)
func main() {
doc := `{"flags": [{
"key": "dark-mode",
"enabled": true,
"variants": [{"name": "on"}, {"name": "off"}],
"defaultVariant": "off",
"rules": [{"conditions": [{"attribute": "beta", "op": "eq", "value": true}], "variant": "on"}]
}]}`
flags, err := featureflags.ParseJSON(strings.NewReader(doc))
if err != nil {
panic(err)
}
snapshot, err := featureflags.NewSnapshot(flags)
if err != nil {
panic(err)
}
r := snapshot.Evaluate("dark-mode", featureflags.EvalContext{Attributes: map[string]any{"beta": true}})
fmt.Println(r.Variant.Name, r.Reason)
}
Output: on rule_match
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager is the management API: it validates definitions before they reach the Store and emits change events. It is safe for concurrent use if the Store is.
func NewManager ¶
func NewManager(store Store, opts ManagerOptions) (*Manager, error)
NewManager returns a Manager writing to store.
type ManagerOptions ¶
type ManagerOptions struct {
// OnChange runs after every successful write with the write's context.
// When the write joins a caller's database transaction (see
// pgxstore.WithConn), the hook can record the audit entry in the same
// transaction; an error returned by the hook is returned by the write
// method, together with the stored flag, so the caller can roll back.
// Without a surrounding transaction the write has already happened when
// the hook fails.
OnChange func(ctx context.Context, e ChangeEvent) error
// Now stamps ChangeEvent.At; time.Now when nil.
Now func() time.Time
}
ManagerOptions configures a Manager. The zero value is ready to use.
type Notifier ¶
Notifier pushes change signals so a running Client refreshes without waiting for the next poll. Watch blocks until ctx is done, calling changed whenever flags may have changed (also after every reconnect, since changes made while disconnected were missed). changed must be cheap and must not block. Watch returns nil when ctx is done and an error only when it cannot continue at all.
type Operator ¶
type Operator string
Operator is the comparison a Condition applies to an attribute.
const ( // OpEquals holds when the attribute equals Value. OpEquals Operator = "eq" // OpNotEquals holds when the attribute is present and differs from Value. OpNotEquals Operator = "neq" // OpIn holds when the attribute equals one of Values. OpIn Operator = "in" // OpNotIn holds when the attribute is present and equals none of Values. OpNotIn Operator = "not_in" // OpContains holds when a string attribute contains the string Value as a // substring, or when a slice attribute has an element equal to Value. OpContains Operator = "contains" // OpLessThan holds when a numeric attribute is less than the number Value. OpLessThan Operator = "lt" // OpLessOrEqual holds when a numeric attribute is at most Value. OpLessOrEqual Operator = "lte" // OpGreaterThan holds when a numeric attribute is greater than Value. OpGreaterThan Operator = "gt" // OpGreaterOrEqual holds when a numeric attribute is at least Value. OpGreaterOrEqual Operator = "gte" )
Supported operators. Equality treats all Go numeric types and json.Number as the same number, so an int attribute equals a float64 value of the same magnitude. Values of different kinds (string against number) are never equal. A condition on an attribute that is absent from the EvalContext, or nil, never holds, for every operator including OpNotEquals and OpNotIn.
type Options ¶
type Options struct {
// PollInterval is how often Run reloads from the source. Zero means
// DefaultPollInterval; a negative value disables polling, leaving
// refreshes to the Notifier and explicit Refresh calls.
PollInterval time.Duration
// RefreshTimeout bounds each refresh performed by Run, so a hung source
// cannot stall later refreshes. Zero means DefaultRefreshTimeout.
RefreshTimeout time.Duration
// Notifier, when set, triggers refreshes from pushed change signals
// while Run is active.
Notifier Notifier
// OnEvaluation runs synchronously on the request path after every
// evaluation through the Client. It must be fast and must not block;
// hand events to a buffered channel for anything slow.
OnEvaluation func(ctx context.Context, e EvaluationEvent)
// OnError receives refresh failures and rejected flag definitions. It
// must not block. Errors are discarded when it is nil.
OnError func(err error)
// Now is the clock used for LastRefresh; time.Now when nil.
Now func() time.Time
}
Options configures a Client. The zero value is ready to use.
type Reason ¶
type Reason string
Reason explains why an evaluation served its variant.
const ( // ReasonFlagNotFound: the snapshot has no flag with the key. The result // carries the zero Variant, and Client.Enabled reports false. ReasonFlagNotFound Reason = "flag_not_found" // ReasonDisabled: the kill switch is off; the default variant is served. ReasonDisabled Reason = "disabled" // ReasonTargetMatch: the user or tenant is listed in Targets[Index]. ReasonTargetMatch Reason = "target_match" // ReasonRuleMatch: Rules[Index] matched and served the variant. ReasonRuleMatch Reason = "rule_match" // ReasonRollout: the flag-level Rollout distribution chose the variant. ReasonRollout Reason = "rollout" // ReasonDefault: nothing chose a variant; the default variant is served. ReasonDefault Reason = "default" )
Evaluation reasons.
type Result ¶
type Result struct {
// Key is the evaluated flag key.
Key string
// Variant is the served variant; the zero Variant when the flag was not
// found. Its payload is shared with the snapshot and must not be modified.
Variant Variant
// Reason explains the choice.
Reason Reason
// Index is the position of the matching target (ReasonTargetMatch) or
// rule (ReasonRuleMatch) and -1 for every other reason.
Index int
// Version is the version of the flag definition that was evaluated, 0
// when the flag was not found or was never stored.
Version int64
}
Result is the outcome of evaluating one flag.
type Rule ¶
type Rule struct {
Conditions []Condition `json:"conditions,omitempty"`
Variant string `json:"variant,omitempty"`
Distribution *Distribution `json:"distribution,omitempty"`
}
Rule serves a variant to contexts that satisfy all of its Conditions. A rule with no conditions matches every context.
Exactly one of Variant and Distribution is set. A rule with a Distribution only applies to contexts that have the distribution's bucketing key; other contexts move on to the next rule.
type Snapshot ¶
type Snapshot struct {
// contains filtered or unexported fields
}
Snapshot is an immutable, validated set of flags evaluated entirely in memory. It is safe for concurrent use. The zero Snapshot holds no flags.
func NewSnapshot ¶
NewSnapshot validates and compiles flags into a Snapshot. It fails if any flag is invalid or two flags share a key. The flags are copied, so the caller may reuse the slice.
func (*Snapshot) Evaluate ¶
func (s *Snapshot) Evaluate(key string, ec EvalContext) Result
Evaluate resolves the flag key for ec. A missing flag yields ReasonFlagNotFound and the zero Variant. Evaluate never blocks and never performs I/O.
type Source ¶
Source loads the complete set of flag definitions. Every Store is a Source; FileSource and SourceFunc adapt other origins.
func FileSource ¶
FileSource returns a Source that reads and parses the named file from fsys with ParseJSON on every Load, so a polling Client picks up edits. Use os.DirFS for files on disk, or an embed.FS to ship flags in the binary. Replace the file atomically (write a temporary file, then rename it) so a refresh never reads a partial file; a partial file would fail to parse and the Client would keep its current snapshot.
type SourceFunc ¶
SourceFunc adapts a function to the Source interface.
type Store ¶
type Store interface {
Source
// Get returns the flag stored under key, or an error wrapping
// ErrFlagNotFound.
Get(ctx context.Context, key string) (Flag, error)
// Create stores a new flag at version 1, ignoring f.Version, and returns
// it with Version and UpdatedAt set.
Create(ctx context.Context, f Flag) (Flag, error)
// Update replaces the definition of f.Key if its stored version equals
// f.Version, and returns it with the incremented Version and new
// UpdatedAt.
Update(ctx context.Context, f Flag) (Flag, error)
// Delete removes key if its stored version equals version.
Delete(ctx context.Context, key string, version int64) error
}
Store persists flag definitions with optimistic concurrency. The storetest package holds a conformance suite for implementations.
Stores enforce key format (ErrInvalidFlag), uniqueness (ErrFlagExists) and version checks (ErrVersionConflict) themselves, but full definition validation is the Manager's job; write through a Manager.
type Target ¶
type Target struct {
Variant string `json:"variant"`
Users []string `json:"users,omitempty"`
Tenants []string `json:"tenants,omitempty"`
}
Target serves Variant to every context whose user ID is in Users or whose tenant ID is in Tenants.
type ValidationError ¶
ValidationError lists every problem found in one flag definition. It matches ErrInvalidFlag with errors.Is.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string
Error returns all problems on one line.
func (*ValidationError) Is ¶
func (e *ValidationError) Is(target error) bool
Is makes errors.Is(err, ErrInvalidFlag) true.
type Variant ¶
type Variant struct {
// Name is 1 to 64 characters from A-Z, a-z, 0-9, '.', '_' and '-',
// starting with a letter or digit.
Name string `json:"name"`
// Payload is optional JSON. Payloads returned by evaluation are shared
// with the snapshot and must not be modified.
Payload json.RawMessage `json:"payload,omitempty"`
}
Variant is one value a flag can serve: a name plus an optional JSON payload, for example a configuration object for an experiment arm.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package featureflagstest gives application tests a real *featureflags.Client whose flags they control directly, without a store, polling or background goroutines.
|
Package featureflagstest gives application tests a real *featureflags.Client whose flags they control directly, without a store, polling or background goroutines. |
|
Package memstore is an in-memory featureflags.Store for tests, examples and single-process tools.
|
Package memstore is an in-memory featureflags.Store for tests, examples and single-process tools. |
|
Package pgxstore is a PostgreSQL featureflags.Store built on pgx v5, with a LISTEN/NOTIFY Listener that pushes changes to running clients.
|
Package pgxstore is a PostgreSQL featureflags.Store built on pgx v5, with a LISTEN/NOTIFY Listener that pushes changes to running clients. |
|
Package storetest is a conformance suite for featureflags.Store implementations.
|
Package storetest is a conformance suite for featureflags.Store implementations. |