- Add MetricsPage and useOnboarding hook - Update superadmin controller and service - Add module documentation (docs/01-modulos/) - Add CONTEXT-MAP.yml and Sprint 5 execution report - Update project status and task traces 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
226 lines
5.1 KiB
Markdown
226 lines
5.1 KiB
Markdown
# SAAS-009: Feature Flags
|
|
|
|
## Metadata
|
|
- **Codigo:** SAAS-009
|
|
- **Modulo:** Feature Flags
|
|
- **Prioridad:** P2
|
|
- **Estado:** Pendiente
|
|
- **Fase:** 4 - Advanced
|
|
|
|
## Descripcion
|
|
|
|
Sistema de feature flags para control granular de funcionalidades: flags globales, por tenant, por usuario, con porcentaje de rollout y A/B testing basico.
|
|
|
|
## Objetivos
|
|
|
|
1. Flags globales (sistema)
|
|
2. Flags por tenant
|
|
3. Flags por usuario
|
|
4. Porcentaje de rollout
|
|
5. A/B testing basico
|
|
|
|
## Alcance
|
|
|
|
### Incluido
|
|
- Feature flags booleanos
|
|
- Flags con valores (string/number)
|
|
- Override por tenant
|
|
- Override por usuario
|
|
- Rollout gradual (%)
|
|
- UI de administracion
|
|
|
|
### Excluido
|
|
- A/B testing avanzado - usar servicio dedicado
|
|
- Analytics de experimentos
|
|
- Machine learning para targeting
|
|
|
|
## Modelo de Datos
|
|
|
|
### Tablas (schema: features)
|
|
|
|
**feature_flags**
|
|
- id, key (unique), name, description
|
|
- type (boolean/string/number/json)
|
|
- default_value, is_enabled
|
|
- rollout_percentage (0-100)
|
|
- targeting_rules (JSONB)
|
|
- created_at, updated_at
|
|
|
|
**tenant_feature_overrides**
|
|
- id, tenant_id, feature_key
|
|
- value, enabled
|
|
- expires_at
|
|
|
|
**user_feature_overrides**
|
|
- id, user_id, feature_key
|
|
- value, enabled
|
|
- expires_at
|
|
|
|
## Flags Predefinidos
|
|
|
|
| Key | Tipo | Default | Descripcion |
|
|
|-----|------|---------|-------------|
|
|
| new_dashboard | boolean | false | Nuevo dashboard beta |
|
|
| ai_assistant | boolean | false | Asistente IA |
|
|
| export_v2 | boolean | false | Nueva exportacion |
|
|
| max_file_size_mb | number | 10 | Limite archivo |
|
|
| theme | string | 'light' | Tema default |
|
|
|
|
## Endpoints API
|
|
|
|
| Metodo | Endpoint | Descripcion |
|
|
|--------|----------|-------------|
|
|
| GET | /features | Listar flags |
|
|
| GET | /features/:key | Obtener flag |
|
|
| POST | /features | Crear flag |
|
|
| PUT | /features/:key | Actualizar flag |
|
|
| DELETE | /features/:key | Eliminar flag |
|
|
| GET | /features/evaluate | Evaluar todos para usuario |
|
|
| POST | /features/:key/override/tenant | Override tenant |
|
|
| POST | /features/:key/override/user | Override usuario |
|
|
| DELETE | /features/:key/override/tenant | Quitar override |
|
|
|
|
## Implementacion
|
|
|
|
### Servicio
|
|
```typescript
|
|
interface FeatureFlagService {
|
|
isEnabled(key: string, context?: EvaluationContext): Promise<boolean>;
|
|
getValue<T>(key: string, context?: EvaluationContext): Promise<T>;
|
|
getAllFlags(context?: EvaluationContext): Promise<Record<string, any>>;
|
|
}
|
|
|
|
interface EvaluationContext {
|
|
tenantId?: string;
|
|
userId?: string;
|
|
attributes?: Record<string, any>;
|
|
}
|
|
```
|
|
|
|
### Guard
|
|
```typescript
|
|
@RequiresFeature('new_dashboard')
|
|
@Get('/dashboard/v2')
|
|
async getNewDashboard() {
|
|
// Solo si feature habilitada
|
|
}
|
|
```
|
|
|
|
### Condicional en Codigo
|
|
```typescript
|
|
if (await this.features.isEnabled('ai_assistant')) {
|
|
// Mostrar boton de IA
|
|
}
|
|
|
|
const maxSize = await this.features.getValue<number>('max_file_size_mb');
|
|
```
|
|
|
|
## Logica de Evaluacion
|
|
|
|
```
|
|
1. Usuario solicita feature
|
|
2. Buscar override de usuario
|
|
3. Si existe, usar ese valor
|
|
4. Si no, buscar override de tenant
|
|
5. Si existe, usar ese valor
|
|
6. Si no, evaluar rollout %
|
|
7. Si pasa rollout, usar default_value
|
|
8. Si no pasa, feature deshabilitada
|
|
```
|
|
|
|
### Rollout Deterministico
|
|
```typescript
|
|
// Mismo usuario siempre obtiene mismo resultado
|
|
function shouldEnableForUser(userId: string, percentage: number): boolean {
|
|
const hash = crypto.createHash('md5').update(userId).digest('hex');
|
|
const value = parseInt(hash.substring(0, 8), 16) % 100;
|
|
return value < percentage;
|
|
}
|
|
```
|
|
|
|
## Targeting Rules
|
|
|
|
```typescript
|
|
// Ejemplo de reglas avanzadas
|
|
{
|
|
"rules": [
|
|
{
|
|
"condition": "plan",
|
|
"operator": "in",
|
|
"values": ["pro", "enterprise"],
|
|
"enabled": true
|
|
},
|
|
{
|
|
"condition": "country",
|
|
"operator": "equals",
|
|
"values": ["MX"],
|
|
"enabled": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Frontend SDK
|
|
|
|
```typescript
|
|
// React hook
|
|
function useFeatureFlag(key: string): boolean {
|
|
const { flags } = useFeatureFlags();
|
|
return flags[key] ?? false;
|
|
}
|
|
|
|
// Componente
|
|
function Dashboard() {
|
|
const showNewDashboard = useFeatureFlag('new_dashboard');
|
|
|
|
return showNewDashboard ? <NewDashboard /> : <OldDashboard />;
|
|
}
|
|
```
|
|
|
|
## Entregables
|
|
|
|
| Entregable | Estado | Archivo |
|
|
|------------|--------|---------|
|
|
| features.module.ts | Pendiente | `modules/features/` |
|
|
| feature-flag.service.ts | Pendiente | `services/` |
|
|
| feature.guard.ts | Pendiente | `guards/` |
|
|
| DDL features schema | Pendiente | `ddl/schemas/features/` |
|
|
| Seeds flags default | Pendiente | `seeds/prod/features/` |
|
|
|
|
## Dependencias
|
|
|
|
### Depende de
|
|
- SAAS-001 (Auth)
|
|
- SAAS-002 (Tenants)
|
|
- SAAS-003 (Users)
|
|
|
|
### Bloquea a
|
|
- Rollouts graduales
|
|
- Experimentos de producto
|
|
|
|
## Criterios de Aceptacion
|
|
|
|
- [ ] Flags booleanos funcionan
|
|
- [ ] Override por tenant funciona
|
|
- [ ] Override por usuario funciona
|
|
- [ ] Rollout % es deterministico
|
|
- [ ] UI admin permite gestion
|
|
- [ ] Cache funciona correctamente
|
|
|
|
## Caching
|
|
|
|
```typescript
|
|
// Redis cache con invalidacion
|
|
const cacheKey = `features:${tenantId}:${userId}`;
|
|
const cached = await redis.get(cacheKey);
|
|
if (cached) return JSON.parse(cached);
|
|
|
|
const flags = await this.evaluateAllFlags(context);
|
|
await redis.setex(cacheKey, 300, JSON.stringify(flags)); // 5 min
|
|
return flags;
|
|
```
|
|
|
|
---
|
|
|
|
**Ultima actualizacion:** 2026-01-07
|