Files
amayo/README/FEATURE_FLAGS_SYSTEM.md
Shni 89d475ba66 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.
2025-10-31 21:12:27 -05:00

13 KiB

🎮 Feature Flags System

Sistema completo de Feature Flags para control de funcionalidades, rollouts progresivos, A/B testing y toggles dinámicos.

📋 Índice


🚀 Instalación

1. Migración de Base de Datos

npx prisma migrate dev --name add_feature_flags

2. Inicialización del Servicio

En tu src/loaders/ o punto de entrada principal:

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

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

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

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

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)

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)

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

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

// 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.

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.

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.

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.

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

// ❌ Mal
'flag_1'
'test'
'new'

// ✅ Bien
'new_shop_ui_v2'
'improved_combat_algorithm'
'halloween_2025_event'

2. Siempre con Descripción

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

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

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:

await featureFlagService.refreshCache();

🔥 Casos de Uso Reales

Lanzamiento de Comando Nuevo

// 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

// 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

// 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

// 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

// 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:

// 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:

import { loadFeatureFlags } from './loaders/featureFlagsLoader';

// ...
await loadFeatureFlags();
// ...

Creado con 🎮 para el bot Amayo