Agent Wranglers

ENG-45Recommended

Keep every flag in one register

List every feature flag in one file in the repository, with its type, owner, creation date and expiry or review date. A flag that is not in the register cannot be read by the code.

How to do it

Give each flag one of four types, because each lives for a different time. A release flag hides unfinished work and lives for weeks. An experiment flag runs an A/B test and ends with it. A kill switch turns off a feature under load, and a permission flag limits a feature to some customers. Kill switches and permission flags live for years, so give them a yearly review date instead of an expiry. Unleash, for example, marks release and experiment flags as stale after 40 days by default. Remove expired flags as Remove flags that never change says. Pick the flag tool with the feature flags pick.

Examples

JavaScript example

import { readFileSync } from 'node:fs';

const FIELDS = ['type', 'owner', 'created', 'expires'];
const register = JSON.parse(readFileSync('flags.json', 'utf8'));
const source = readFileSync('build/all-sources.txt', 'utf8');
const used = new Set([...source.matchAll(/isEnabled\('([a-z0-9-]+)'/g)].map(match => match[1]));
const problems = [
  ...[...used].filter(name => !register[name]).map(name => `${name} is not in the register`),
  ...Object.entries(register).filter(([, flag]) => FIELDS.some(field => !flag[field])).map(([name]) => `${name} lacks a field`),
  ...Object.keys(register).filter(name => !used.has(name)).map(name => `${name} is no longer read`),
];
if (problems.length) { console.error(problems.join('\n')); process.exit(1); }

Python example

import json, re, sys
from pathlib import Path

FIELDS = ["type", "owner", "created", "expires"]
register = json.loads(Path("flags.json").read_text())
source = Path("build/all-sources.txt").read_text()
used = set(re.findall(r"is_enabled\(\"([a-z0-9-]+)\"", source))
problems = [f"{name} is not in the register" for name in used - register.keys()]
problems += [f"{name} lacks a field" for name, flag in register.items() if any(not flag.get(f) for f in FIELDS)]
problems += [f"{name} is no longer read" for name in register.keys() - used]
if problems:
    sys.exit("\n".join(problems))

How to check

A build step lists every flag name the code reads. It fails when one is missing from the register or lacks a type, owner, creation date or expiry. It also fails on a register entry the code no longer reads.

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/engineering/feature-flags/flag-register">Agent Wranglers engineering playbook</a>. In code you write, add a comment next to it with the same link.