flag

package
v1.8.2 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 17 Imported by: 1

Documentation

Overview

Package flag provides feature flag definitions, targeting rules, and evaluation.

Index

Constants

View Source
const (
	ReasonDisabled       = "disabled"
	ReasonTenantOverride = "tenantOverride"
	ReasonRule           = "rule"
	ReasonDefault        = "default"
)

Reasons a flag evaluated the way it did. These are the four outcomes the engine can reach, in the order it reaches them.

Variables

This section is empty.

Functions

func RolloutBucket

func RolloutBucket(tenantID, flagKey string) uint32

RolloutBucket returns the deterministic 0-99 bucket a tenant falls into for a flag. A rollout rule matches when the bucket is below its percentage. Exported so an explained evaluation can report the same number the verdict used; two copies of this arithmetic would eventually disagree.

func ValidateValue

func ValidateValue(t Type, v any) error

ValidateValue reports whether v is an acceptable value for a flag of type t. It exists because no backend checks: a bool flag whose default is the string "true" is stored happily, and every typed read then silently answers the caller's fallback.

A value that arrives through JSON is a float64, so int accepts a float64 with no fractional part, up to 2^53 in magnitude, as well as any Go integer. nil is refused for every type except json, where it is the JSON null.

Types

type ContextKey

type ContextKey = scope.ContextKey

ContextKey is the context key type for flag evaluation. It is the same type as scope.ContextKey, so a tenant or user set with the scope helpers is the one the engine reads.

const (
	// ContextKeyTenantID is the context key for tenant ID.
	ContextKeyTenantID ContextKey = "vault.tenant_id"
	// ContextKeyUserID is the context key for user ID.
	ContextKeyUserID ContextKey = "vault.user_id"
)

type CreateInput

type CreateInput struct {
	Key          string
	Type         Type
	DefaultValue any
	Description  string
	Tags         []string
	Enabled      bool
}

CreateInput is everything a new flag carries. DefaultValue must match Type.

type Definition

type Definition struct {
	core.Entity
	ID           id.ID             `json:"id"`
	Key          string            `json:"key"`
	Type         Type              `json:"type"`
	DefaultValue any               `json:"default_value"`
	Description  string            `json:"description"`
	Tags         []string          `json:"tags,omitempty"`
	Variants     []Variant         `json:"variants,omitempty"`
	Enabled      bool              `json:"enabled"`
	AppID        string            `json:"app_id"`
	Metadata     map[string]string `json:"metadata,omitempty"`
}

Definition represents a feature flag definition.

type Detail

type Detail struct {
	Value       any         `json:"value"`
	Reason      string      `json:"reason"`
	MatchedRule *Rule       `json:"matched_rule,omitempty"`
	Trace       []TraceStep `json:"trace"`
}

Detail is an explained evaluation: the value, why it was chosen, the rule that chose it when one did, and every rule considered on the way. MatchedRule is nil unless Reason is ReasonRule.

type Engine

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

Engine evaluates feature flags using definitions, rules, and overrides.

func NewEngine

func NewEngine(store Store, opts ...EngineOption) *Engine

NewEngine creates a flag evaluation engine.

func (*Engine) Evaluate

func (e *Engine) Evaluate(ctx context.Context, key, appID string) (any, error)

Evaluate returns the value for a flag key and app ID. Evaluation order: disabled -> tenant override -> rules (by priority) -> default.

This is the hot path every SDK flag read goes through, so it keeps the evaluation cache. EvaluateDetail deliberately does not.

func (*Engine) EvaluateDetail

func (e *Engine) EvaluateDetail(ctx context.Context, key, appID string) (Detail, error)

EvaluateDetail returns the value together with why it was chosen and every rule considered. It always bypasses the evaluation cache, because it exists to answer "why is this tenant seeing this right now" and a cached answer is the wrong answer to that question.

func (*Engine) Invalidate

func (e *Engine) Invalidate(flagKey string)

Invalidate drops every cached evaluation for a flag, across apps, tenants and users. Call it after changing a flag's definition, rules or tenant overrides so Evaluate callers see the change now rather than after the TTL. An Evaluate that was already in flight when Invalidate ran may still return the old value to its own caller, but it does not cache it. It is a no-op when the engine has no cache.

type EngineOption

type EngineOption func(*Engine)

EngineOption configures the Engine.

func WithCacheMaxEntries

func WithCacheMaxEntries(n int) EngineOption

WithCacheMaxEntries bounds how many evaluation results the cache holds at once. n <= 0 means the default of 10000. It has no effect unless the cache is also enabled with WithCacheTTL, and it works regardless of the order the two options are given in: the cache is built once, after every option has run.

func WithCacheTTL

func WithCacheTTL(ttl time.Duration) EngineOption

WithCacheTTL sets the evaluation cache TTL, enabling the cache.

func WithMetrics

func WithMetrics(m gometrics.Metrics) EngineOption

WithMetrics counts evaluations on m by where the answer came from (the cache or the store) and by result. In a Forge app pass app.Metrics(); vault's Forge extension does this for you. A nil m records nothing. No label ever carries a flag key, a tenant or a user.

type ListOpts

type ListOpts struct {
	Limit  int
	Offset int
	AppID  string

	// Type restricts the list to flags of one value type. Empty means every
	// type.
	Type Type
}

ListOpts configures list queries for flag definitions.

type Manager

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

Manager is the flag write service. Every backend's DefineFlag is an upsert that never checks anything, so the store on its own will overwrite a flag with a different type, accept a default that does not match the type, and leave rules behind for a key that has no flag. Manager is the one path that refuses those writes, drops the evaluation cache after each change, and reports it to an audit hook.

It is safe for concurrent use as far as its store is. Update and SetEnabled are read-modify-write of one row with no version check, so two concurrent writers to the same flag can lose one write.

func NewManager

func NewManager(store Store, engine *Engine, opts ...ManagerOption) *Manager

NewManager builds a flag write service over store. engine may be nil, in which case no evaluation cache is invalidated.

func (*Manager) Create

func (m *Manager) Create(ctx context.Context, in CreateInput) (*Definition, error)

Create defines a new flag. It refuses an invalid key, type or default, and returns core.ErrFlagExists (vault.ErrFlagExists) when the key is taken, rather than overwriting the flag the way the store's upsert would.

func (*Manager) Delete

func (m *Manager) Delete(ctx context.Context, key string) error

Delete removes a flag together with its rules and overrides.

func (*Manager) DeleteTenantOverride

func (m *Manager) DeleteTenantOverride(ctx context.Context, key, tenantID string) error

DeleteTenantOverride removes a tenant's override. A tenant with none returns core.ErrOverrideNotFound from the store.

func (*Manager) SetEnabled

func (m *Manager) SetEnabled(ctx context.Context, key string, enabled bool) (*Definition, error)

SetEnabled turns a flag on or off. Setting the value it already has writes nothing and records nothing.

func (*Manager) SetRules

func (m *Manager) SetRules(ctx context.Context, key string, rules []RuleInput) ([]*Rule, error)

SetRules replaces the flag's whole rule list. Priority is the index, so the first rule wins. Every rule is validated before anything is written.

func (*Manager) SetTenantOverride

func (m *Manager) SetTenantOverride(ctx context.Context, key, tenantID string, value any) (*TenantOverride, error)

SetTenantOverride pins a flag's value for one tenant. The flag must exist: no backend checks that for an override.

func (*Manager) Update

func (m *Manager) Update(ctx context.Context, key string, in UpdateInput) (*Definition, error)

Update changes the description, default value or tags of an existing flag. Only non-nil fields change; the rest of the row (variants, metadata, id, creation time) is carried over untouched. A new default is validated against the flag's stored type.

type ManagerOption

type ManagerOption func(*Manager)

ManagerOption configures a Manager.

func WithManagerAppID

func WithManagerAppID(appID string) ManagerOption

WithManagerAppID sets the app every flag the manager touches belongs to. It is not called WithAppID because that name already configures the read Service in this package.

func WithOnFlagMutate

func WithOnFlagMutate(fn func(ctx context.Context, action, key, appID, tenantID string)) ManagerOption

WithOnFlagMutate registers a callback invoked after each successful write, with the context the write ran under and one of the flag.* audit actions. tenantID is the tenant a per-tenant override write targets, and empty for every write to the flag itself.

type Rule

type Rule struct {
	core.Entity
	ID          id.ID      `json:"id"`
	FlagKey     string     `json:"flag_key"`
	AppID       string     `json:"app_id"`
	Priority    int        `json:"priority"` // lower = higher priority
	Type        RuleType   `json:"type"`
	Config      RuleConfig `json:"config"`
	ReturnValue any        `json:"return_value"`
}

Rule represents a targeting rule for a feature flag. Rules are evaluated in priority order (lower number = higher priority).

func Rollout

func Rollout(percentage int) *Rule

Rollout creates a rule for percentage-based rollout (0-100).

func Schedule

func Schedule(start, end time.Time) *Rule

Schedule creates a rule active only within a time window.

func WhenTenant

func WhenTenant(tenantIDs ...string) *Rule

WhenTenant creates a rule that matches specific tenant IDs.

func WhenTenantTag

func WhenTenantTag(key, value string) *Rule

WhenTenantTag creates a rule that matches tenants with a specific tag.

func WhenUser

func WhenUser(userIDs ...string) *Rule

WhenUser creates a rule that matches specific user IDs.

func (*Rule) Return

func (r *Rule) Return(value any) *Rule

Return sets the value returned when this rule matches.

type RuleConfig

type RuleConfig struct {
	// WhenTenant: match specific tenant IDs.
	TenantIDs []string `json:"tenant_ids,omitempty"`

	// WhenTenantTag: match tenants with a specific tag.
	TagKey   string `json:"tag_key,omitempty"`
	TagValue string `json:"tag_value,omitempty"`

	// WhenUser: match specific user IDs.
	UserIDs []string `json:"user_ids,omitempty"`

	// Rollout: percentage (0-100).
	Percentage int `json:"percentage,omitempty"`

	// Schedule: time-based activation.
	StartAt *time.Time `json:"start_at,omitempty"`
	EndAt   *time.Time `json:"end_at,omitempty"`

	// Custom: plugin-evaluated.
	Evaluator string         `json:"evaluator,omitempty"` // plugin name
	Params    map[string]any `json:"params,omitempty"`
}

RuleConfig holds the configuration for a targeting rule.

type RuleInput

type RuleInput struct {
	Type        RuleType
	Config      RuleConfig
	ReturnValue any
}

RuleInput is one targeting rule as a caller writes it. Priority is not an input: SetRules derives it from the rule's index.

type RuleType

type RuleType string

RuleType represents the type of a targeting rule.

const (
	RuleWhenTenant    RuleType = "when_tenant"
	RuleWhenTenantTag RuleType = "when_tenant_tag"
	RuleWhenUser      RuleType = "when_user"
	RuleRollout       RuleType = "rollout"
	RuleSchedule      RuleType = "schedule"
	RuleCustom        RuleType = "custom"
)

Targeting rule types.

type Service

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

Service provides type-safe feature flag evaluation.

func NewService

func NewService(engine *Engine, opts ...ServiceOption) *Service

NewService creates a type-safe flag evaluation service.

func (*Service) Bool

func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) bool

Bool evaluates a boolean flag. Returns defaultVal on error or type mismatch.

func (*Service) Float

func (s *Service) Float(ctx context.Context, key string, defaultVal float64) float64

Float evaluates a float flag. Returns defaultVal on error or type mismatch. Handles both float64 and int (promoted to float64).

func (*Service) Int

func (s *Service) Int(ctx context.Context, key string, defaultVal int) int

Int evaluates an integer flag. Returns defaultVal on error or type mismatch. Handles both int and float64 (from JSON unmarshaling).

func (*Service) JSON

func (s *Service) JSON(ctx context.Context, key string, target any) error

JSON evaluates a JSON flag and unmarshals it into target. target must be a pointer. Returns an error if evaluation fails or the value cannot be marshaled to JSON and unmarshaled into target.

func (*Service) String

func (s *Service) String(ctx context.Context, key, defaultVal string) string

String evaluates a string flag. Returns defaultVal on error or type mismatch.

type ServiceOption

type ServiceOption func(*Service)

ServiceOption configures the flag Service.

func WithAppID

func WithAppID(appID string) ServiceOption

WithAppID sets the default app ID for the flag service.

type Store

type Store interface {
	// DefineFlag creates or updates a flag definition.
	DefineFlag(ctx context.Context, f *Definition) error

	// GetFlagDefinition retrieves a flag definition by key and app ID.
	GetFlagDefinition(ctx context.Context, key, appID string) (*Definition, error)

	// ListFlagDefinitions returns the flag definitions for an app in key
	// order, filtered by opts.Type and then paged. An empty result is an
	// empty slice, never nil.
	ListFlagDefinitions(ctx context.Context, appID string, opts ListOpts) ([]*Definition, error)

	// DeleteFlagDefinition removes a flag definition and its rules.
	DeleteFlagDefinition(ctx context.Context, key, appID string) error

	// SetFlagRules replaces all targeting rules for a flag.
	SetFlagRules(ctx context.Context, key, appID string, rules []*Rule) error

	// GetFlagRules returns targeting rules for a flag, ordered by priority.
	GetFlagRules(ctx context.Context, key, appID string) ([]*Rule, error)

	// SetFlagTenantOverride sets a direct per-tenant override value.
	SetFlagTenantOverride(ctx context.Context, key, appID, tenantID string, value any) error

	// GetFlagTenantOverride retrieves a tenant override for a flag.
	GetFlagTenantOverride(ctx context.Context, key, appID, tenantID string) (any, error)

	// DeleteFlagTenantOverride removes a tenant override.
	DeleteFlagTenantOverride(ctx context.Context, key, appID, tenantID string) error

	// ListFlagTenantOverrides returns all tenant overrides for a flag.
	ListFlagTenantOverrides(ctx context.Context, key, appID string) ([]*TenantOverride, error)

	// CountFlagDefinitions returns the total number of flag definitions for
	// an app, independent of any paging.
	CountFlagDefinitions(ctx context.Context, appID string) (int64, error)

	// CountFlagDefinitionsMatching returns the number of flag definitions for
	// an app that match opts.Type. Limit and Offset are ignored, so it is the
	// total behind a page returned by ListFlagDefinitions with the same opts.
	CountFlagDefinitionsMatching(ctx context.Context, appID string, opts ListOpts) (int64, error)
}

Store defines the persistence interface for feature flags.

type TenantOverride

type TenantOverride struct {
	core.Entity
	ID       id.ID  `json:"id"`
	FlagKey  string `json:"flag_key"`
	AppID    string `json:"app_id"`
	TenantID string `json:"tenant_id"`
	Value    any    `json:"value"`
}

TenantOverride represents a direct per-tenant flag value override.

type TraceStep

type TraceStep struct {
	RuleID   string   `json:"rule_id"`
	Priority int      `json:"priority"`
	Type     RuleType `json:"type"`
	Matched  bool     `json:"matched"`
	Reached  bool     `json:"reached"`
	Note     string   `json:"note,omitempty"`
}

TraceStep records one targeting rule the engine considered. RuleID is the rule's own id, so a step can be tied back to the rule it describes. Reached is false for rules after the one that matched: the engine stops at the first match, and a trace that hid that would misreport precedence.

type Type

type Type string

Type represents the value type of a feature flag.

const (
	TypeBool   Type = "bool"
	TypeString Type = "string"
	TypeInt    Type = "int"
	TypeFloat  Type = "float"
	TypeJSON   Type = "json"
)

Flag value types.

type UpdateInput

type UpdateInput struct {
	Description  *string
	DefaultValue *any
	Tags         *[]string
}

UpdateInput changes only the fields that are non-nil.

type ValidationError

type ValidationError struct {
	Field   string
	Message string
}

ValidationError is a refusal of caller input. Field names the part of the request at fault in the wire's own spelling (for example "rules[0].returnValue"), so a handler can hand it straight to the client.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error returns "flag: <field>: <message>".

type Variant

type Variant struct {
	Value       any    `json:"value"`
	Description string `json:"description"`
}

Variant represents a named flag variant.

Jump to

Keyboard shortcuts

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