Documentation
¶
Overview ¶
Package flag provides feature flag definitions, targeting rules, and evaluation.
Index ¶
- Constants
- func RolloutBucket(tenantID, flagKey string) uint32
- func ValidateValue(t Type, v any) error
- type ContextKey
- type CreateInput
- type Definition
- type Detail
- type Engine
- type EngineOption
- type ListOpts
- type Manager
- func (m *Manager) Create(ctx context.Context, in CreateInput) (*Definition, error)
- func (m *Manager) Delete(ctx context.Context, key string) error
- func (m *Manager) DeleteTenantOverride(ctx context.Context, key, tenantID string) error
- func (m *Manager) SetEnabled(ctx context.Context, key string, enabled bool) (*Definition, error)
- func (m *Manager) SetRules(ctx context.Context, key string, rules []RuleInput) ([]*Rule, error)
- func (m *Manager) SetTenantOverride(ctx context.Context, key, tenantID string, value any) (*TenantOverride, error)
- func (m *Manager) Update(ctx context.Context, key string, in UpdateInput) (*Definition, error)
- type ManagerOption
- type Rule
- type RuleConfig
- type RuleInput
- type RuleType
- type Service
- func (s *Service) Bool(ctx context.Context, key string, defaultVal bool) bool
- func (s *Service) Float(ctx context.Context, key string, defaultVal float64) float64
- func (s *Service) Int(ctx context.Context, key string, defaultVal int) int
- func (s *Service) JSON(ctx context.Context, key string, target any) error
- func (s *Service) String(ctx context.Context, key, defaultVal string) string
- type ServiceOption
- type Store
- type TenantOverride
- type TraceStep
- type Type
- type UpdateInput
- type ValidationError
- type Variant
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) DeleteTenantOverride ¶
DeleteTenantOverride removes a tenant's override. A tenant with none returns core.ErrOverrideNotFound from the store.
func (*Manager) SetEnabled ¶
SetEnabled turns a flag on or off. Setting the value it already has writes nothing and records nothing.
func (*Manager) SetRules ¶
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 WhenTenant ¶
WhenTenant creates a rule that matches specific tenant IDs.
func WhenTenantTag ¶
WhenTenantTag creates a rule that matches tenants with a specific tag.
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 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) Float ¶
Float evaluates a float flag. Returns defaultVal on error or type mismatch. Handles both float64 and int (promoted to float64).
func (*Service) Int ¶
Int evaluates an integer flag. Returns defaultVal on error or type mismatch. Handles both int and float64 (from JSON unmarshaling).
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 UpdateInput ¶
UpdateInput changes only the fields that are non-nil.
type ValidationError ¶
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>".