This document describes the feature flag system implemented for the Glean Developer Site.
- Edge Config Name:
glean-developer-feature-flags - Add the connection string to your environment as
EDGE_CONFIG
{
"feature-flags": {
"new-api-docs": {
"enabled": true,
"rolloutPercentage": 50
},
"beta-tutorials": {
"enabled": false
},
"mcp-cli-version": {
"enabled": true,
"metadata": {
"version": "1.0.0-beta.1"
},
"description": "Version to pin the @gleanwork/configure-mcp-server package to"
}
}
}FF_NEW_API_DOCS=true FF_BETA_TUTORIALS=false pnpm buildFEATURE_FLAGS_JSON='{"new-api-docs":{"enabled":true}}' pnpm buildHide sidebar sections:
// sidebars.ts
{
type: 'category',
label: 'Beta Features',
customProps: {
flag: 'beta-features' // Hidden unless flag is enabled
},
items: [...]
}Hide navbar items:
// docusaurus.config.ts
{
label: 'Beta',
href: '/beta',
flag: 'beta-docs' // Hidden unless flag is enabled
}In MDX files:
<FeatureFlag flag="new-feature">
## This content is gated
Only shows when flag is enabled.
</FeatureFlag>In React components:
import { useContext } from 'react';
import { FeatureFlagsContext } from '@site/src/theme/Root';
function Component() {
const { isEnabled } = useContext(FeatureFlagsContext);
if (isEnabled('new-ui')) {
return <NewUI />;
}
return <OldUI />;
}Create .env file:
# Edge Config (optional locally)
EDGE_CONFIG=ecfg_xxxxx
# Override flags locally
FEATURE_FLAGS_JSON='{"my-flag":{"enabled":true}}'
FF_BETA_DOCS=true
# Enable debug mode
FLAGS_DEBUG=truesrc/lib/featureFlags.ts- Core evaluation logicsrc/lib/featureFlagTypes.ts- TypeScript typessrc/theme/Root.tsx- React Context providersrc/components/FeatureFlag.tsx- MDX componentsrc/utils/buildTimeFlags.ts- Build-time flag loadersrc/utils/filtering.ts- Sidebar/navbar filteringapi/feature-flags.js- Vercel API endpoint
-
Build Time:
- Reads flags from env vars
- Filters sidebars/navbar
- Embeds snapshot in build
-
Runtime:
- Hydrates from build snapshot (instant)
- Fetches from
/api/feature-flagsin background - Caches in localStorage (5 min TTL)
- CDN caches API responses (60s)
{
"flag-name": {
"description": "Optional description",
"enabled": boolean,
"rolloutPercentage": 0-100, // Optional
"allowedUsers": ["email"], // Optional
"metadata": {} // Optional
}
}Testing with team:
{
"enabled": true,
"allowedUsers": ["team@glean.com"]
}25% rollout:
{
"enabled": true,
"rolloutPercentage": 25
}Kill switch:
{
"enabled": false
}Using metadata for configuration values:
{
"enabled": true,
"metadata": {
"version": "1.0.0-beta.1",
"apiUrl": "https://api.example.com",
"maxRetries": 3
}
}The metadata field can store arbitrary configuration values that can be accessed in components:
const { flagConfigs } = useContext(FeatureFlagsContext);
const version = flagConfigs['mcp-cli-version']?.metadata?.version as string | undefined;With FLAGS_DEBUG=true:
- Check console for active flags
- Override via URL:
?ff_my-feature-flag=true&ff_my-other-feature-flag=true - Access
window.__FLAGS_DEBUG__
Our rollout system uses deterministic bucketing to ensure users get a consistent experience:
-
User Identity: We identify users by (in order of preference):
- Email address (if logged in)
- User ID (if available)
- Visitor ID (anonymous, stored in localStorage)
-
Hash Calculation: For each flag, we create a hash from
flag-name:user-identity- This produces a number between 0-99
- The same user always gets the same number for the same flag
- Different flags produce different numbers for the same user
-
Rollout Decision:
- If
rolloutPercentage: 25, users with hash 0-24 see the feature - If
rolloutPercentage: 50, users with hash 0-49 see the feature - This ensures a stable, random distribution
- If
- Consistent Experience: A user won't see features flickering on/off between page loads
- True Randomization: Users are distributed evenly across different features
- Cross-Device: If using email/userID, the same user gets the same experience on all devices
- Anonymous Support: Works even for logged-out users via persistent visitor ID
User "alice@glean.com" with two flags:
Flag "new-search" (50% rollout):
Hash("new-search:alice@glean.com") = 23 → ENABLED ✓
Flag "beta-ui" (30% rollout):
Hash("beta-ui:alice@glean.com") = 67 → DISABLED ✗
Alice will always see "new-search" but never "beta-ui" unless the percentages change.
- Build-time flags: Zero runtime cost, best for SEO
- Runtime flags: ~5ms evaluation, cached aggressively
- Hash calculation: <1ms using fast non-cryptographic hash
- Start:
allowedUsers: ["team@glean.com"] - Expand:
rolloutPercentage: 10 - Increase:
rolloutPercentage: 50 - Launch:
enabled: true(remove restrictions)
{
"variant-a": { "enabled": true, "rolloutPercentage": 50 },
"variant-b": { "enabled": true, "rolloutPercentage": 50 }
}- Add flag with
enabled: true - Wrap old feature in
!isEnabled('new-feature') - After migration, remove flag and old code
Flags not working at build:
- Check env vars are exported
- Verify JSON syntax in
FEATURE_FLAGS_JSON - Look for flag evaluation in build logs
Flags not updating at runtime:
- Check
/api/feature-flagsresponse - Verify
EDGE_CONFIGis set in Vercel - Clear localStorage to force refresh
- Check browser console with
FLAGS_DEBUG=true