Off means off — no combobox role, no listener, no fetch. A test asserts the absence of the request, not just the absence of the dropdown, because a hidden-but-fetching control would still be spending the rate limit on a feature nobody can see. Enter with no active option falls through to the form's submit handler and searches the typed text exactly as before. The existing behaviour is preserved, not replaced, and that has its own test. Suppressed once the value parses as a postcode: the box takes a name OR a postcode, and suggesting schools during postcode entry fights the user. .omniBoxContainer gains position: relative — the dropdown is absolutely positioned and without it would have anchored to the page instead. Four render sites, all wired: page.tsx renders HomeView in the success path AND the catch fallback, and HomeView renders FilterBar as hero AND sticky. Missing any one would make the flag silently do nothing somewhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015mWQnpye9F299NVRCCSRvj
119 lines
4.1 KiB
Python
119 lines
4.1 KiB
Python
"""Feature flags: what can be switched, and what is switched right now.
|
|
|
|
Ship-dark, not a kill switch. Flags let work merge and deploy without becoming
|
|
visible; they are expected to flip about monthly, by a person, deliberately.
|
|
Nothing here does percentage rollouts or user targeting — the site has no user
|
|
identity to target.
|
|
|
|
Unleash holds the state. It does not hold the list. REGISTRY below is that
|
|
list, and it exists for three reasons: the SDK evaluates an unknown flag to
|
|
False, so without a registry that is an *undeclared* False, indistinguishable
|
|
from a typo; /api/flags needs a key set to return when Unleash is unreachable;
|
|
and a flag in the UI but not in the registry is orphaned and should be visibly
|
|
so rather than quietly authoritative.
|
|
|
|
Every flag defaults to False. There is no per-flag default, because a flag that
|
|
defaults on is a kill switch, and this is not one.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from dataclasses import dataclass
|
|
from datetime import date
|
|
|
|
from .config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# A flag is temporary scaffolding. See test_a_flag_older_than_the_limit.
|
|
MAX_FLAG_AGE_DAYS = 90
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Flag:
|
|
# One string: the registry key, the Unleash flag name, and the JSON key in
|
|
# /api/flags. snake_case, matching the API's existing convention. No case
|
|
# transformation anywhere, so there is no mapping layer to get wrong.
|
|
name: str
|
|
description: str # one line: what turning this on reveals
|
|
added: date # for the staleness tripwire
|
|
|
|
|
|
REGISTRY: dict[str, Flag] = {
|
|
f.name: f for f in (
|
|
Flag(
|
|
name="admission_distance",
|
|
description=(
|
|
"The last-distance-offered figure on the Admissions tile and "
|
|
"the 'How far away are you?' section on school pages."
|
|
),
|
|
added=date(2026, 8, 23),
|
|
),
|
|
Flag(
|
|
name="school_autosuggest",
|
|
description=(
|
|
"School name suggestions as you type in the main search box."
|
|
),
|
|
added=date(2026, 8, 26),
|
|
),
|
|
)
|
|
}
|
|
|
|
|
|
_client = None
|
|
|
|
|
|
def init() -> None:
|
|
"""Start the Unleash client, or log why flags are all off.
|
|
|
|
Called once from the app lifespan. Never raises: a flag system that can
|
|
stop the API from booting is worse than one that is switched off.
|
|
"""
|
|
global _client
|
|
if not settings.unleash_url or not settings.unleash_api_token:
|
|
logger.warning(
|
|
"Unleash is not configured (UNLEASH_URL / UNLEASH_API_TOKEN); "
|
|
"every feature flag evaluates to False.")
|
|
return
|
|
|
|
try:
|
|
from UnleashClient import UnleashClient
|
|
|
|
_client = UnleashClient(
|
|
url=settings.unleash_url,
|
|
app_name=settings.unleash_app_name,
|
|
custom_headers={"Authorization": settings.unleash_api_token},
|
|
cache_directory=settings.unleash_cache_directory,
|
|
refresh_interval=15,
|
|
)
|
|
_client.initialize_client()
|
|
logger.info("Unleash client initialised against %s", settings.unleash_url)
|
|
except Exception:
|
|
# Fail closed and keep serving. The SDK also evaluates everything False
|
|
# until its first successful sync, so this is the same direction.
|
|
_client = None
|
|
logger.exception("Unleash client failed to start; flags are all False.")
|
|
|
|
|
|
def is_enabled(name: str) -> bool:
|
|
"""Whether `name` is on. False for anything unknown, unreachable or broken."""
|
|
if name not in REGISTRY:
|
|
logger.error(
|
|
"undeclared feature flag %r was evaluated; returning False. "
|
|
"Add it to backend/flags.py REGISTRY or fix the name.", name)
|
|
return False
|
|
if _client is None:
|
|
return False
|
|
try:
|
|
return bool(_client.is_enabled(
|
|
name, fallback_function=lambda feature_name, context: False))
|
|
except Exception:
|
|
logger.exception("flag %r failed to evaluate; returning False", name)
|
|
return False
|
|
|
|
|
|
def all_flags() -> dict[str, bool]:
|
|
"""Every declared flag and its current value. Serves /api/flags."""
|
|
return {name: is_enabled(name) for name in REGISTRY}
|