feat: Implement feature flag system with helpers, service, and loader
- Added feature flag helpers and decorators for easy usage in commands. - Created a feature flag service for managing flags, including initialization, caching, and evaluation strategies. - Implemented a loader to initialize the feature flag service on bot startup. - Defined types for feature flags, including configurations, contexts, evaluations, and statistics. - Provided examples of feature flag usage in commands, demonstrating various patterns such as A/B testing, gradual rollouts, and access control.
This commit is contained in:
626
README/FEATURE_FLAGS_SYSTEM.md
Normal file
626
README/FEATURE_FLAGS_SYSTEM.md
Normal file
@@ -0,0 +1,626 @@
|
||||
# 🎮 Feature Flags System
|
||||
|
||||
Sistema completo de Feature Flags para control de funcionalidades, rollouts progresivos, A/B testing y toggles dinámicos.
|
||||
|
||||
## 📋 Índice
|
||||
|
||||
- [Instalación](#instalación)
|
||||
- [Conceptos](#conceptos)
|
||||
- [Uso Básico](#uso-básico)
|
||||
- [Ejemplos Avanzados](#ejemplos-avanzados)
|
||||
- [Comando de Administración](#comando-de-administración)
|
||||
- [Estrategias de Rollout](#estrategias-de-rollout)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalación
|
||||
|
||||
### 1. Migración de Base de Datos
|
||||
|
||||
```bash
|
||||
npx prisma migrate dev --name add_feature_flags
|
||||
```
|
||||
|
||||
### 2. Inicialización del Servicio
|
||||
|
||||
En tu `src/loaders/` o punto de entrada principal:
|
||||
|
||||
```typescript
|
||||
import { featureFlagService } from '@/core/services/FeatureFlagService';
|
||||
|
||||
// Inicializar el servicio
|
||||
await featureFlagService.initialize();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Conceptos
|
||||
|
||||
### Estados de Flags
|
||||
|
||||
- **`enabled`**: Habilitado para todos
|
||||
- **`disabled`**: Deshabilitado para todos
|
||||
- **`rollout`**: Rollout progresivo según estrategia
|
||||
- **`maintenance`**: Deshabilitado por mantenimiento
|
||||
|
||||
### Targets
|
||||
|
||||
- **`global`**: Aplica a todo el bot
|
||||
- **`guild`**: Aplica por servidor
|
||||
- **`user`**: Aplica por usuario
|
||||
- **`channel`**: Aplica por canal
|
||||
|
||||
### Estrategias de Rollout
|
||||
|
||||
- **`percentage`**: Basado en % de usuarios
|
||||
- **`whitelist`**: Solo IDs específicos
|
||||
- **`blacklist`**: Todos excepto IDs específicos
|
||||
- **`gradual`**: Rollout gradual en el tiempo
|
||||
- **`random`**: Aleatorio por sesión
|
||||
|
||||
---
|
||||
|
||||
## 💡 Uso Básico
|
||||
|
||||
### 1. En Comandos con Decorador
|
||||
|
||||
```typescript
|
||||
import { RequireFeature } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
class ShopCommand {
|
||||
@RequireFeature('new_shop_system', {
|
||||
fallbackMessage: '🔧 El nuevo sistema de tienda estará disponible pronto'
|
||||
})
|
||||
async execute(interaction: CommandInteraction) {
|
||||
// Este código solo se ejecuta si el flag está habilitado
|
||||
await interaction.reply('Bienvenido a la nueva tienda!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Con Guard en el Handler
|
||||
|
||||
```typescript
|
||||
import { featureGuard } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function handleMineCommand(interaction: CommandInteraction) {
|
||||
// Check del flag
|
||||
if (!await featureGuard('new_mining_system', interaction)) {
|
||||
return; // Automáticamente responde al usuario
|
||||
}
|
||||
|
||||
// Código del comando nuevo
|
||||
await doNewMining(interaction);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Check Manual
|
||||
|
||||
```typescript
|
||||
import { isFeatureEnabledForInteraction } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function execute(interaction: CommandInteraction) {
|
||||
const useNewAlgorithm = await isFeatureEnabledForInteraction(
|
||||
'improved_algorithm',
|
||||
interaction
|
||||
);
|
||||
|
||||
if (useNewAlgorithm) {
|
||||
await newAlgorithm();
|
||||
} else {
|
||||
await oldAlgorithm();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Ejemplos Avanzados
|
||||
|
||||
### A/B Testing
|
||||
|
||||
```typescript
|
||||
import { abTest, extractContext } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function handleShop(interaction: CommandInteraction) {
|
||||
const context = extractContext(interaction);
|
||||
|
||||
const result = await abTest('new_shop_ui', context, {
|
||||
variant: async () => {
|
||||
// Nueva UI
|
||||
return buildNewShopUI();
|
||||
},
|
||||
control: async () => {
|
||||
// UI antigua
|
||||
return buildOldShopUI();
|
||||
}
|
||||
});
|
||||
|
||||
await interaction.reply(result);
|
||||
}
|
||||
```
|
||||
|
||||
### Múltiples Flags (AND)
|
||||
|
||||
```typescript
|
||||
import { requireAllFeatures, extractContext } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function handlePremiumFeature(interaction: CommandInteraction) {
|
||||
const context = extractContext(interaction);
|
||||
|
||||
const hasAccess = await requireAllFeatures(
|
||||
['premium_features', 'beta_access', 'new_ui'],
|
||||
context
|
||||
);
|
||||
|
||||
if (!hasAccess) {
|
||||
await interaction.reply('No tienes acceso a esta funcionalidad');
|
||||
return;
|
||||
}
|
||||
|
||||
// Código de la feature premium
|
||||
}
|
||||
```
|
||||
|
||||
### Múltiples Flags (OR)
|
||||
|
||||
```typescript
|
||||
import { requireAnyFeature, extractContext } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function handleSpecialEvent(interaction: CommandInteraction) {
|
||||
const context = extractContext(interaction);
|
||||
|
||||
const hasEventAccess = await requireAnyFeature(
|
||||
['halloween_event', 'christmas_event', 'beta_events'],
|
||||
context
|
||||
);
|
||||
|
||||
if (!hasEventAccess) {
|
||||
await interaction.reply('No hay eventos activos para ti');
|
||||
return;
|
||||
}
|
||||
|
||||
// Código del evento
|
||||
}
|
||||
```
|
||||
|
||||
### Con Fallback
|
||||
|
||||
```typescript
|
||||
import { withFeature, extractContext } from '@/core/lib/featureFlagHelpers';
|
||||
|
||||
async function getData(interaction: CommandInteraction) {
|
||||
const context = extractContext(interaction);
|
||||
|
||||
const data = await withFeature(
|
||||
'new_data_source',
|
||||
context,
|
||||
async () => {
|
||||
// Fuente nueva
|
||||
return fetchFromNewAPI();
|
||||
},
|
||||
async () => {
|
||||
// Fuente antigua (fallback)
|
||||
return fetchFromOldAPI();
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Comando de Administración
|
||||
|
||||
### Crear un Flag
|
||||
|
||||
```
|
||||
/featureflags create name:new_shop_system status:disabled target:global description:"Nuevo sistema de tienda"
|
||||
```
|
||||
|
||||
### Listar Flags
|
||||
|
||||
```
|
||||
/featureflags list
|
||||
```
|
||||
|
||||
### Ver Info de un Flag
|
||||
|
||||
```
|
||||
/featureflags info flag:new_shop_system
|
||||
```
|
||||
|
||||
### Actualizar Estado
|
||||
|
||||
```
|
||||
/featureflags update flag:new_shop_system status:enabled
|
||||
```
|
||||
|
||||
### Configurar Rollout Progresivo
|
||||
|
||||
```
|
||||
/featureflags rollout flag:new_shop_system strategy:percentage percentage:25
|
||||
```
|
||||
|
||||
Esto habilitará la feature para el 25% de los usuarios.
|
||||
|
||||
### Configurar Rollout Gradual
|
||||
|
||||
```typescript
|
||||
// Programáticamente
|
||||
await featureFlagService.setFlag({
|
||||
name: 'new_combat_system',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'gradual',
|
||||
rolloutConfig: {
|
||||
gradual: {
|
||||
startPercentage: 10, // Empieza con 10%
|
||||
targetPercentage: 100, // Llega al 100%
|
||||
durationDays: 7 // En 7 días
|
||||
}
|
||||
},
|
||||
startDate: new Date()
|
||||
});
|
||||
```
|
||||
|
||||
### Ver Estadísticas
|
||||
|
||||
```
|
||||
/featureflags stats flag:new_shop_system
|
||||
```
|
||||
|
||||
### Refrescar Caché
|
||||
|
||||
```
|
||||
/featureflags refresh
|
||||
```
|
||||
|
||||
### Eliminar Flag
|
||||
|
||||
```
|
||||
/featureflags delete flag:old_feature
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Estrategias de Rollout
|
||||
|
||||
### 1. Percentage (Porcentaje)
|
||||
|
||||
Distribuye la feature a un % de usuarios de forma determinista.
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'feature_x',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'percentage',
|
||||
rolloutConfig: {
|
||||
percentage: 50 // 50% de usuarios
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Whitelist (Lista Blanca)
|
||||
|
||||
Solo para IDs específicos.
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'beta_features',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'whitelist',
|
||||
rolloutConfig: {
|
||||
targetIds: [
|
||||
'123456789', // User ID 1
|
||||
'987654321' // User ID 2
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Blacklist (Lista Negra)
|
||||
|
||||
Para todos excepto IDs específicos.
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'stable_feature',
|
||||
status: 'rollout',
|
||||
target: 'guild',
|
||||
rolloutStrategy: 'blacklist',
|
||||
rolloutConfig: {
|
||||
targetIds: [
|
||||
'guild_id_problematico'
|
||||
]
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Gradual (Progresivo en el Tiempo)
|
||||
|
||||
Rollout gradual durante X días.
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'major_update',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'gradual',
|
||||
rolloutConfig: {
|
||||
gradual: {
|
||||
startPercentage: 5, // Empieza con 5%
|
||||
targetPercentage: 100, // Termina en 100%
|
||||
durationDays: 14 // Durante 14 días
|
||||
}
|
||||
},
|
||||
startDate: new Date() // Importante: define cuándo empieza
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Best Practices
|
||||
|
||||
### 1. Nombres Claros y Descriptivos
|
||||
|
||||
```typescript
|
||||
// ❌ Mal
|
||||
'flag_1'
|
||||
'test'
|
||||
'new'
|
||||
|
||||
// ✅ Bien
|
||||
'new_shop_ui_v2'
|
||||
'improved_combat_algorithm'
|
||||
'halloween_2025_event'
|
||||
```
|
||||
|
||||
### 2. Siempre con Descripción
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'new_mining_system',
|
||||
description: 'Sistema de minería rediseñado con durabilidad de herramientas',
|
||||
status: 'disabled',
|
||||
target: 'global'
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Rollouts Graduales para Cambios Grandes
|
||||
|
||||
Para cambios importantes, usa rollout gradual:
|
||||
|
||||
1. Día 1-3: 10% de usuarios
|
||||
2. Día 4-7: 50% de usuarios
|
||||
3. Día 8-14: 100% de usuarios
|
||||
|
||||
### 4. Limpiar Flags Obsoletos
|
||||
|
||||
Una vez que una feature está 100% desplegada y estable:
|
||||
|
||||
1. Elimina el flag
|
||||
2. Elimina el código del check
|
||||
3. Mantén solo la nueva implementación
|
||||
|
||||
### 5. Usar Whitelists para Beta Testers
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'experimental_features',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'whitelist',
|
||||
rolloutConfig: {
|
||||
targetIds: BETA_TESTER_IDS // Array de tus beta testers
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 6. Fechas de Expiración para Eventos
|
||||
|
||||
```typescript
|
||||
await featureFlagService.setFlag({
|
||||
name: 'christmas_2025_event',
|
||||
status: 'enabled',
|
||||
target: 'global',
|
||||
startDate: new Date('2025-12-01'),
|
||||
endDate: new Date('2025-12-31')
|
||||
});
|
||||
```
|
||||
|
||||
El flag se auto-deshabilitará después del 31 de diciembre.
|
||||
|
||||
### 7. Caché y Performance
|
||||
|
||||
El servicio cachea flags en memoria por 5 minutos. Si necesitas actualizaciones inmediatas:
|
||||
|
||||
```
|
||||
/featureflags refresh
|
||||
```
|
||||
|
||||
O programáticamente:
|
||||
|
||||
```typescript
|
||||
await featureFlagService.refreshCache();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔥 Casos de Uso Reales
|
||||
|
||||
### Lanzamiento de Comando Nuevo
|
||||
|
||||
```typescript
|
||||
// Fase 1: Desarrollo - Deshabilitado
|
||||
await featureFlagService.setFlag({
|
||||
name: 'pvp_arena_command',
|
||||
status: 'disabled',
|
||||
target: 'global'
|
||||
});
|
||||
|
||||
// Fase 2: Beta Testing - Solo whitelisted
|
||||
await featureFlagService.setFlag({
|
||||
name: 'pvp_arena_command',
|
||||
status: 'rollout',
|
||||
target: 'guild',
|
||||
rolloutStrategy: 'whitelist',
|
||||
rolloutConfig: {
|
||||
targetIds: ['guild_beta_1', 'guild_beta_2']
|
||||
}
|
||||
});
|
||||
|
||||
// Fase 3: Rollout Progresivo - 25%
|
||||
await featureFlagService.setFlag({
|
||||
name: 'pvp_arena_command',
|
||||
status: 'rollout',
|
||||
target: 'user',
|
||||
rolloutStrategy: 'percentage',
|
||||
rolloutConfig: { percentage: 25 }
|
||||
});
|
||||
|
||||
// Fase 4: Habilitado para Todos
|
||||
await featureFlagService.setFlag({
|
||||
name: 'pvp_arena_command',
|
||||
status: 'enabled',
|
||||
target: 'global'
|
||||
});
|
||||
|
||||
// Fase 5: Cleanup - Eliminar flag y código del check
|
||||
await featureFlagService.removeFlag('pvp_arena_command');
|
||||
```
|
||||
|
||||
### Migración de Sistema Antiguo a Nuevo
|
||||
|
||||
```typescript
|
||||
// En el comando
|
||||
async function handleInventory(interaction: CommandInteraction) {
|
||||
const context = extractContext(interaction);
|
||||
|
||||
await abTest('inventory_system_v2', context, {
|
||||
variant: async () => {
|
||||
// Sistema nuevo
|
||||
return await newInventorySystem.show(interaction);
|
||||
},
|
||||
control: async () => {
|
||||
// Sistema antiguo
|
||||
return await oldInventorySystem.show(interaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Luego gradualmente aumentas el % hasta 100% y eliminas el código antiguo.
|
||||
|
||||
### Kill Switch para Emergencias
|
||||
|
||||
```typescript
|
||||
// Si hay un bug crítico en una feature:
|
||||
await featureFlagService.setFlag({
|
||||
name: 'problematic_feature',
|
||||
status: 'maintenance', // Deshabilitado inmediatamente
|
||||
target: 'global'
|
||||
});
|
||||
|
||||
// O via comando Discord:
|
||||
// /featureflags update flag:problematic_feature status:maintenance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 API Reference
|
||||
|
||||
Ver `src/core/types/featureFlags.ts` para tipos completos.
|
||||
|
||||
### FeatureFlagService
|
||||
|
||||
```typescript
|
||||
// Inicializar
|
||||
await featureFlagService.initialize();
|
||||
|
||||
// Check si está habilitado
|
||||
const enabled = await featureFlagService.isEnabled('flag_name', context);
|
||||
|
||||
// Crear/actualizar flag
|
||||
await featureFlagService.setFlag(config);
|
||||
|
||||
// Eliminar flag
|
||||
await featureFlagService.removeFlag('flag_name');
|
||||
|
||||
// Obtener flag
|
||||
const flag = featureFlagService.getFlag('flag_name');
|
||||
|
||||
// Obtener todos los flags
|
||||
const flags = featureFlagService.getFlags();
|
||||
|
||||
// Estadísticas
|
||||
const stats = featureFlagService.getStats('flag_name');
|
||||
const allStats = featureFlagService.getAllStats();
|
||||
|
||||
// Refrescar caché
|
||||
await featureFlagService.refreshCache();
|
||||
featureFlagService.clearEvaluationCache();
|
||||
```
|
||||
|
||||
### Helpers
|
||||
|
||||
```typescript
|
||||
// Check básico
|
||||
await isFeatureEnabled(flagName, context);
|
||||
await isFeatureEnabledForInteraction(flagName, interaction);
|
||||
|
||||
// Guards
|
||||
await featureGuard(flagName, interaction, options);
|
||||
|
||||
// Decorador
|
||||
@RequireFeature('flag_name', options)
|
||||
|
||||
// A/B Testing
|
||||
await abTest(flagName, context, { variant, control });
|
||||
|
||||
// Wrapper
|
||||
await withFeature(flagName, context, fn, fallback);
|
||||
|
||||
// Múltiples flags
|
||||
await requireAllFeatures(flags, context); // AND
|
||||
await requireAnyFeature(flags, context); // OR
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Integración con tu Bot
|
||||
|
||||
El sistema se integra automáticamente si añades la inicialización en tu loader:
|
||||
|
||||
```typescript
|
||||
// src/loaders/featureFlagsLoader.ts
|
||||
import { featureFlagService } from '../services/FeatureFlagService';
|
||||
import logger from '../lib/logger';
|
||||
|
||||
export async function loadFeatureFlags() {
|
||||
try {
|
||||
await featureFlagService.initialize();
|
||||
logger.info('[FeatureFlags] Sistema inicializado');
|
||||
} catch (error) {
|
||||
logger.error('[FeatureFlags] Error al inicializar:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Luego en tu `main.ts` o donde cargues servicios:
|
||||
|
||||
```typescript
|
||||
import { loadFeatureFlags } from './loaders/featureFlagsLoader';
|
||||
|
||||
// ...
|
||||
await loadFeatureFlags();
|
||||
// ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Creado con 🎮 para el bot Amayo
|
||||
Reference in New Issue
Block a user