flags

package
v0.46.0 Latest Latest
Warning

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

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

Documentation

Overview

Package flags provides feature flags stored in the app's own database: typed flags declared in code, per-user evaluation with an allow-list, roles and sticky percentage rollout, and an in-memory cache kept fresh by database change listeners. See docs/proposed-features/feature-flags.md.

Index

Constants

View Source
const Channel = "hamr_feature_flags"

Channel is the Postgres NOTIFY channel the trigger publishes on.

View Source
const PostgresTriggerSQL = `CREATE OR REPLACE FUNCTION hamr_feature_flags_notify() RETURNS trigger AS $$
BEGIN
	PERFORM pg_notify('` + Channel + `', '');
	RETURN NULL;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS hamr_feature_flags_notify ON ` + Table + `;
CREATE TRIGGER hamr_feature_flags_notify
	AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON ` + Table + `
	FOR EACH STATEMENT EXECUTE FUNCTION hamr_feature_flags_notify();`

PostgresTriggerSQL creates the trigger that NOTIFYs Channel on every change to Table. It is idempotent and multi-statement. The scaffold's migration inlines the same SQL.

View Source
const Table = "feature_flags"

Table is the flags table the scaffold creates.

Variables

View Source
var ErrInvalid = errors.New("invalid flag rule")

ErrInvalid wraps every validation error from Set. Its messages are safe to show to the person editing the rule; store errors are not.

Functions

func DevAction

func DevAction(c *Client) devactions.Action

DevAction returns a "set flag" action for the hamr dev widget. The flag list is read here, so declare flags before calling it.

func Override

func Override[T any](ctx context.Context, f *Flag[T], v T) context.Context

Override returns a ctx on which f always evaluates to v. For tests.

Types

type Client

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

Client holds the in-memory rules and keeps them fresh.

func New

func New(store Store, opts ...Option) *Client

New returns a Client over store. Call Start before serving.

func (*Client) For

func (c *Client) For(ctx context.Context, w Who) context.Context

For returns a ctx evaluating flags through c for w. For background jobs.

func (*Client) Middleware

func (c *Client) Middleware(who WhoFunc) echo.MiddlewareFunc

Middleware puts c and the request's Who on the request context, and adds every flag's value to the request logger as a "flags" group.

func (*Client) Rules

func (c *Client) Rules() []Rule

Rules returns copies of the current rules, sorted by key.

func (*Client) Set

func (c *Client) Set(ctx context.Context, r Rule) error

Set validates r against the declared flag, saves it and updates the local cache immediately. Validation errors wrap ErrInvalid.

func (*Client) Start

func (c *Client) Start(ctx context.Context) error

Start loads all rules and starts the poll and the watcher until ctx ends. A load error is returned, but the client stays usable: it serves code defaults and keeps retrying.

type Definition

type Definition struct {
	Key     string
	Type    Type
	Default json.RawMessage
}

Definition describes a declared flag, for admin pages and dev actions.

func Definitions

func Definitions() []Definition

Definitions returns every declared flag, sorted by key.

type Flag

type Flag[T any] struct {
	// contains filtered or unexported fields
}

Flag is a typed flag declaration. It holds only the key and the code default; live values come from the Client carried on the context.

func Bool

func Bool(key string, def bool) *Flag[bool]

Bool declares a bool flag. Like every declaration, it panics if key is already declared or is not lowercase letters, digits, '_', '.' or '-'.

func Int

func Int(key string, def int) *Flag[int]

Int declares an int flag. It panics if key is already declared.

func JSON

func JSON[T any](key string, def T) *Flag[T]

JSON declares a flag whose value is any JSON-decodable T. It panics if key is already declared. Each Get decodes a fresh T, so callers may mutate it.

func String

func String(key string, def string) *Flag[string]

String declares a string flag. It panics if key is already declared.

func (*Flag[T]) Default

func (f *Flag[T]) Default() T

Default returns the code default (a fresh copy for JSON flags).

func (*Flag[T]) Get

func (f *Flag[T]) Get(ctx context.Context) T

Get returns the flag's value for the user on ctx. It never errors: with no Client on ctx, a disabled rule, or a user who doesn't match, it returns the code default.

func (*Flag[T]) Key

func (f *Flag[T]) Key() string

Key returns the flag's key.

type Option

type Option func(*Client)

Option configures a Client.

func WithAnonymousCookie

func WithAnonymousCookie(name string) Option

WithAnonymousCookie makes the middleware set a random first-party ID cookie with this name for logged-out visitors, so percentage rollout applies to them. The cookie is likely not "strictly necessary" under ePrivacy: the app may need consent before enabling it.

func WithDirectURL

func WithDirectURL(url string) Option

WithDirectURL makes the Postgres watcher open its own connection to url instead of using the pool. Use it behind PgBouncer in transaction mode, where LISTEN does not work.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger for warnings. Default slog.Default().

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval sets the safety-net full reload interval. Default 5m.

func WithPostgres

func WithPostgres(db *sql.DB) Option

WithPostgres watches db (a pgx-backed *sql.DB) with LISTEN and reloads on every change. The listener pins one pooled connection, so the pool needs at least two (or use WithDirectURL); on a one-connection pool it logs an error and does not listen, leaving only the poll.

func WithSQLite

func WithSQLite(db *sql.DB) Option

WithSQLite watches db by polling PRAGMA data_version on one pinned connection and reloads when another connection has committed. ponytail: data_version ignores this connection's own commits (Set updates the cache itself) and a 1s poll means up to 1s staleness. With a single-connection pool (hamr's default) the connection is borrowed per tick instead of pinned, since pinning would starve the app.

type Rule

type Rule struct {
	Key      string
	Enabled  bool
	Value    json.RawMessage
	Subjects []string
	Roles    []string
	Percent  int // 0–100
}

Rule is the stored state of one flag. When Enabled and the user matches (in Subjects, has one of Roles, or hashes under Percent), Get returns Value; otherwise the code default.

type Store

type Store interface {
	List(ctx context.Context) ([]Rule, error)
	Save(ctx context.Context, r Rule) error
}

Store persists rules. The app implements it over its own table.

func NewMemStore

func NewMemStore(rules ...Rule) Store

NewMemStore returns an in-memory Store seeded with rules. For tests.

type Type

type Type string

Type names the value type of a flag.

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

type Who

type Who struct {
	SubjectID string
	Roles     []string
}

Who identifies the user flags are evaluated for.

type WhoFunc

type WhoFunc func(c echo.Context) Who

WhoFunc resolves the request's user. Run Middleware after auth.

Jump to

Keyboard shortcuts

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