ADR-024 Feature Flagging Strategy
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 22m13s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 22m13s
This commit is contained in:
@@ -4,16 +4,17 @@ Common code patterns extracted from Architecture Decision Records (ADRs). Use th
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Pattern | Key Function/Class | Import From |
|
||||
| ------------------ | ------------------------------------------------- | ---------------------------------- |
|
||||
| Error Handling | `handleDbError()`, `NotFoundError` | `src/services/db/errors.db.ts` |
|
||||
| Repository Methods | `get*`, `find*`, `list*` | `src/services/db/*.db.ts` |
|
||||
| API Responses | `sendSuccess()`, `sendPaginated()`, `sendError()` | `src/utils/apiResponse.ts` |
|
||||
| Transactions | `withTransaction()` | `src/services/db/connection.db.ts` |
|
||||
| Validation | `validateRequest()` | `src/middleware/validation.ts` |
|
||||
| Authentication | `authenticateJWT` | `src/middleware/auth.ts` |
|
||||
| Caching | `cacheService` | `src/services/cache.server.ts` |
|
||||
| Background Jobs | Queue classes | `src/services/queues.server.ts` |
|
||||
| Pattern | Key Function/Class | Import From |
|
||||
| ------------------ | ------------------------------------------------- | ------------------------------------- |
|
||||
| Error Handling | `handleDbError()`, `NotFoundError` | `src/services/db/errors.db.ts` |
|
||||
| Repository Methods | `get*`, `find*`, `list*` | `src/services/db/*.db.ts` |
|
||||
| API Responses | `sendSuccess()`, `sendPaginated()`, `sendError()` | `src/utils/apiResponse.ts` |
|
||||
| Transactions | `withTransaction()` | `src/services/db/connection.db.ts` |
|
||||
| Validation | `validateRequest()` | `src/middleware/validation.ts` |
|
||||
| Authentication | `authenticateJWT` | `src/middleware/auth.ts` |
|
||||
| Caching | `cacheService` | `src/services/cache.server.ts` |
|
||||
| Background Jobs | Queue classes | `src/services/queues.server.ts` |
|
||||
| Feature Flags | `isFeatureEnabled()`, `useFeatureFlag()` | `src/services/featureFlags.server.ts` |
|
||||
|
||||
---
|
||||
|
||||
@@ -27,6 +28,7 @@ Common code patterns extracted from Architecture Decision Records (ADRs). Use th
|
||||
- [Authentication](#authentication)
|
||||
- [Caching](#caching)
|
||||
- [Background Jobs](#background-jobs)
|
||||
- [Feature Flags](#feature-flags)
|
||||
|
||||
---
|
||||
|
||||
@@ -491,6 +493,153 @@ const flyerWorker = new Worker(
|
||||
|
||||
---
|
||||
|
||||
## Feature Flags
|
||||
|
||||
**ADR**: [ADR-024](../adr/0024-feature-flagging-strategy.md)
|
||||
|
||||
Feature flags enable controlled feature rollout, A/B testing, and quick production disablement without redeployment. All flags default to `false` (opt-in model).
|
||||
|
||||
### Backend Usage
|
||||
|
||||
```typescript
|
||||
import { isFeatureEnabled, getFeatureFlags } from '../services/featureFlags.server';
|
||||
|
||||
// Check a specific flag in route handler
|
||||
router.get('/dashboard', async (req, res) => {
|
||||
if (isFeatureEnabled('newDashboard')) {
|
||||
return sendSuccess(res, { version: 'v2', data: await getNewDashboardData() });
|
||||
}
|
||||
return sendSuccess(res, { version: 'v1', data: await getLegacyDashboardData() });
|
||||
});
|
||||
|
||||
// Check flag in service layer
|
||||
function processFlyer(flyer: Flyer): ProcessedFlyer {
|
||||
if (isFeatureEnabled('experimentalAi')) {
|
||||
return processWithExperimentalAi(flyer);
|
||||
}
|
||||
return processWithStandardAi(flyer);
|
||||
}
|
||||
|
||||
// Get all flags (admin endpoint)
|
||||
router.get('/admin/feature-flags', requireAdmin, async (req, res) => {
|
||||
sendSuccess(res, { flags: getFeatureFlags() });
|
||||
});
|
||||
```
|
||||
|
||||
### Frontend Usage
|
||||
|
||||
```tsx
|
||||
import { useFeatureFlag, useAllFeatureFlags } from '../hooks/useFeatureFlag';
|
||||
import { FeatureFlag } from '../components/FeatureFlag';
|
||||
|
||||
// Hook approach - for logic beyond rendering
|
||||
function Dashboard() {
|
||||
const isNewDashboard = useFeatureFlag('newDashboard');
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewDashboard) {
|
||||
analytics.track('new_dashboard_viewed');
|
||||
}
|
||||
}, [isNewDashboard]);
|
||||
|
||||
return isNewDashboard ? <NewDashboard /> : <LegacyDashboard />;
|
||||
}
|
||||
|
||||
// Declarative component approach
|
||||
function App() {
|
||||
return (
|
||||
<FeatureFlag feature="newDashboard" fallback={<LegacyDashboard />}>
|
||||
<NewDashboard />
|
||||
</FeatureFlag>
|
||||
);
|
||||
}
|
||||
|
||||
// Debug panel showing all flags
|
||||
function DebugPanel() {
|
||||
const flags = useAllFeatureFlags();
|
||||
return (
|
||||
<ul>
|
||||
{Object.entries(flags).map(([name, enabled]) => (
|
||||
<li key={name}>
|
||||
{name}: {enabled ? 'ON' : 'OFF'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Flag
|
||||
|
||||
1. **Backend** (`src/config/env.ts`):
|
||||
|
||||
```typescript
|
||||
// In featureFlagsSchema
|
||||
myNewFeature: booleanString(false), // FEATURE_MY_NEW_FEATURE
|
||||
|
||||
// In loadEnvVars()
|
||||
myNewFeature: process.env.FEATURE_MY_NEW_FEATURE,
|
||||
```
|
||||
|
||||
2. **Frontend** (`src/config.ts` and `src/vite-env.d.ts`):
|
||||
|
||||
```typescript
|
||||
// In config.ts featureFlags section
|
||||
myNewFeature: import.meta.env.VITE_FEATURE_MY_NEW_FEATURE === 'true',
|
||||
|
||||
// In vite-env.d.ts
|
||||
readonly VITE_FEATURE_MY_NEW_FEATURE?: string;
|
||||
```
|
||||
|
||||
3. **Environment** (`.env.example`):
|
||||
|
||||
```bash
|
||||
# FEATURE_MY_NEW_FEATURE=false
|
||||
# VITE_FEATURE_MY_NEW_FEATURE=false
|
||||
```
|
||||
|
||||
### Testing Feature Flags
|
||||
|
||||
```typescript
|
||||
// Backend - reset modules to test different states
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.env.FEATURE_NEW_DASHBOARD = 'true';
|
||||
});
|
||||
|
||||
// Frontend - mock config module
|
||||
vi.mock('../config', () => ({
|
||||
default: {
|
||||
featureFlags: {
|
||||
newDashboard: true,
|
||||
betaRecipes: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Flag Lifecycle
|
||||
|
||||
| Phase | Actions |
|
||||
| ---------- | -------------------------------------------------------------- |
|
||||
| **Add** | Add to schemas (backend + frontend), default `false`, document |
|
||||
| **Enable** | Set env var `='true'`, restart application |
|
||||
| **Remove** | Remove conditional code, remove from schemas, remove env vars |
|
||||
| **Sunset** | Max 3 months after full rollout - remove flag |
|
||||
|
||||
### Current Flags
|
||||
|
||||
| Flag | Backend Env Var | Frontend Env Var | Purpose |
|
||||
| ---------------- | ------------------------- | ------------------------------ | ------------------------ |
|
||||
| `bugsinkSync` | `FEATURE_BUGSINK_SYNC` | `VITE_FEATURE_BUGSINK_SYNC` | Bugsink error sync |
|
||||
| `advancedRbac` | `FEATURE_ADVANCED_RBAC` | `VITE_FEATURE_ADVANCED_RBAC` | Advanced RBAC features |
|
||||
| `newDashboard` | `FEATURE_NEW_DASHBOARD` | `VITE_FEATURE_NEW_DASHBOARD` | New dashboard experience |
|
||||
| `betaRecipes` | `FEATURE_BETA_RECIPES` | `VITE_FEATURE_BETA_RECIPES` | Beta recipe features |
|
||||
| `experimentalAi` | `FEATURE_EXPERIMENTAL_AI` | `VITE_FEATURE_EXPERIMENTAL_AI` | Experimental AI features |
|
||||
| `debugMode` | `FEATURE_DEBUG_MODE` | `VITE_FEATURE_DEBUG_MODE` | Debug mode |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [ADR Index](../adr/index.md) - All architecture decision records
|
||||
|
||||
Reference in New Issue
Block a user