Expand description
Feature flags — deciding at run time what a given request sees.
This is Laravel Pennant’s job, done for this framework: a named switch, a bit of code that decides it for one user, somewhere to write down an operator’s override, and a route guard.
§These are not Cargo features
The two are easy to confuse and they solve opposite problems.
A Cargo feature is compile time. rustlavel = { features = ["flags"] }
decides what is in the binary; code behind a feature that is off does not
exist in the artifact, cannot be called, and costs nothing. Changing one
means a rebuild and a deploy, and the answer is the same for every request
the binary ever serves.
A flag in this crate is run time. It decides what this request sees, out of code that is already compiled in and shipped. Both branches are in the binary — that is the price — and in exchange the answer can differ per user, change while the process is running, and be turned off by somebody who is not waiting for CI.
Use a Cargo feature to leave a whole package out. Use one of these to ship a half-finished checkout to nobody, then to your own account, then to a quarter of your users, then to everybody, without four deploys.
§The shape of it
// main.rs
let flags = Flags::new()
.define("new-checkout", |scope| async move { scope.id().ends_with('7') })
.define("beta-search", |_| async { false })
.rollout("dark-mode", 25);
App::new()
.plugin(FeatureFlags::from_config(flags, &config))
.routes(|r| {
r.group("/checkout", |r| {
r.middleware(WhenActive::new("new-checkout"));
r.post("", place_order);
});
r.get("/search", |req: Request| async move {
let beta = req.flag("beta-search").await?;
Ok(Response::html(render(beta)))
});
});§The four pieces
- A
Scopeis who a flag is being checked for: a user, a tenant, orScope::nonefor the whole installation. Flagsholds the definitions and answers the question. A per-requestScopedFlagsview remembers what it has resolved, so a flag backed by a slow lookup costs its slowness once a request rather than once a check.- A
FlagStoreholds overrides — an operator’s decision, which beats whatever the resolver computes.MemoryStoreis the one that ships. WhenActivehides a route behind a flag, andFlagsExtlets a handler ask.
§Precedence
The question a flag system exists to answer is “why is this on for this
user?”, so the answer is one short list. It is written out in full, with the
reasoning, on Flags; the summary is that flags.off in configuration
beats everything, then a stored override — with off beating on among them
— then flags.on, then the resolver, then off.
§Rollouts
Flags::rollout is a percentage, and it is stable: the bucket a scope
lands in comes from hashing the flag name with the scope, not from a die
roll, so it is the same on the next request and in the next process. See
rollout for why that matters more than it sounds like it should.
Re-exports§
pub use flags::FlagAnswer;pub use flags::Flags;pub use flags::ScopedFlags;pub use guard::FlagsExt;pub use guard::WhenActive;pub use plugin::FeatureFlags;pub use rollout::bucket;pub use rollout::in_rollout;pub use scope::Scope;pub use store::FlagStore;pub use store::MemoryStore;
Modules§
- flags
- The registry: defining flags, checking them, and overriding the answer.
- guard
- The
WhenActivemiddleware, and asking about a flag from inside a handler. - plugin
- Attaching the package to an application, the way every optional package attaches itself:
- prelude
- Everything an application usually wants:
use rustlavel_flags::prelude::*; - rollout
- Percentage rollouts, and the one property that makes them usable.
- scope
- Who a flag is being checked for.
- store
- Where an answer is written down so it outlives the process.
Enums§
- Error
- Every failure the framework itself can produce.
Type Aliases§
- Result
- The framework-wide result type.