Construire un Système de Feature Flags en 50 Lignes avec Rails.app.creds
Vous n’avez pas besoin de LaunchDarkly. Vous n’avez pas besoin de Flipper. Pour la plupart des applications Rails, vous avez besoin de feature flags versionnés, faciles à surcharger en développement, et qui ne nécessitent pas un autre service à gérer.
Rails.app.creds de Rails 8.2 vous donne exactement cela. Voici un système complet de feature flags en moins de 50 lignes de code.
L’Idée Centrale
Rails.app.creds vérifie d’abord ENV, puis les credentials chiffrées. Cela signifie :
- Stockez vos flags dans
credentials.yml.enc(versionné, revu en PRs) - Surchargez avec des variables ENV à tout moment (test, debug, déploiements progressifs)
Pas de base de données. Pas de service externe. Pas d’appels API.
Étape 1 : Stocker les Flags dans les Credentials
bin/rails credentials:edit
# config/credentials.yml.enc
features:
new_checkout: false
dark_mode: false
ai_summaries: false
beta_dashboard: false
Ce sont vos valeurs par défaut. Elles sont chiffrées, versionnées, et nécessitent une PR pour changer.
Étape 2 : Créer le Module Feature Flags
# app/models/concerns/feature_flags.rb
module FeatureFlags
extend self
def enabled?(flag_name)
value = Rails.app.creds.option(:features, flag_name, default: false)
ActiveModel::Type::Boolean.new.cast(value)
end
def disabled?(flag_name)
!enabled?(flag_name)
end
def enable!(flag_name)
Current.feature_overrides ||= {}
Current.feature_overrides[flag_name] = true
end
def disable!(flag_name)
Current.feature_overrides ||= {}
Current.feature_overrides[flag_name] = false
end
def with(flag_name, value)
old_value = Current.feature_overrides&.dig(flag_name)
enable!(flag_name) if value
disable!(flag_name) unless value
yield
ensure
if old_value.nil?
Current.feature_overrides&.delete(flag_name)
else
Current.feature_overrides[flag_name] = old_value
end
end
end
Étape 3 : Ajouter les Surcharges par Requête
Pour le contrôle des flags par requête (test, surcharges admin), connectez Current :
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :feature_overrides
end
# lib/feature_override_configuration.rb
class FeatureOverrideConfiguration
def require(*keys)
option(*keys)
end
def option(*keys, default: nil)
return default unless keys.first == :features && keys.length == 2
Current.feature_overrides&.dig(keys.last)
end
def keys = []
def reload = nil
end
Mettez à jour la chaîne de credentials :
# config/initializers/credentials.rb
Rails.app.creds = ActiveSupport::CombinedConfiguration.new(
FeatureOverrideConfiguration.new, # Portée requête en premier
Rails.app.envs, # Puis ENV
Rails.app.credentials # Puis fichier chiffré
)
Étape 4 : Utilisez-le Partout
Dans les contrôleurs
class CheckoutController < ApplicationController
def show
if FeatureFlags.enabled?(:new_checkout)
render :new_checkout
else
render :legacy_checkout
end
end
end
Dans les vues
<% if FeatureFlags.enabled?(:dark_mode) %>
<body class="dark">
<% else %>
<body>
<% end %>
Dans les modèles
class Order < ApplicationRecord
def calculate_shipping
if FeatureFlags.enabled?(:new_shipping_calculator)
NewShippingCalculator.calculate(self)
else
legacy_shipping_calculation
end
end
end
Dans les tests
class CheckoutTest < ActionDispatch::IntegrationTest
test "nouveau flux de checkout" do
FeatureFlags.with(:new_checkout, true) do
get checkout_path
assert_select ".new-checkout-form"
end
end
test "flux de checkout legacy" do
FeatureFlags.with(:new_checkout, false) do
get checkout_path
assert_select ".legacy-checkout-form"
end
end
end
Surcharger les Flags
En développement
Surchargez n’importe quel flag sans éditer les credentials :
# Activer un flag
FEATURES__NEW_CHECKOUT=true bin/rails server
# Activer plusieurs
FEATURES__NEW_CHECKOUT=true FEATURES__DARK_MODE=true bin/rails server
En staging/production
Définissez les variables ENV dans votre config de déploiement :
# fly.toml, render.yaml, etc.
[env]
FEATURES__BETA_DASHBOARD = "true"
Par requête (outils admin)
class Admin::FeatureFlagsController < AdminController
def toggle
flag = params[:flag].to_sym
if FeatureFlags.enabled?(flag)
FeatureFlags.disable!(flag)
else
FeatureFlags.enable!(flag)
end
redirect_back fallback_location: admin_root_path
end
end
Avancé : Déploiements par Pourcentage
Vous voulez activer une fonctionnalité pour 10% des utilisateurs ?
# app/models/concerns/feature_flags.rb
module FeatureFlags
def enabled_for?(flag_name, user)
# Vérifier si explicitement activé/désactivé d'abord
return enabled?(flag_name) if explicitly_set?(flag_name)
# Vérifier le déploiement par pourcentage
percentage = Rails.app.creds.option(:features, :"#{flag_name}_percentage", default: nil)
return enabled?(flag_name) unless percentage
# Bucketing cohérent basé sur l'ID utilisateur
bucket = Digest::MD5.hexdigest("#{flag_name}-#{user.id}").first(8).to_i(16) % 100
bucket < percentage.to_i
end
private
def explicitly_set?(flag_name)
Current.feature_overrides&.key?(flag_name) ||
ENV["FEATURES__#{flag_name.to_s.upcase}"].present?
end
end
Configurez le déploiement :
# credentials.yml.enc
features:
new_checkout: false
new_checkout_percentage: 10 # 10% des utilisateurs
Utilisation :
if FeatureFlags.enabled_for?(:new_checkout, current_user)
# 10% des utilisateurs voient ceci
end
Le même utilisateur obtient toujours le même résultat (bucketing cohérent via hash MD5).
Avancé : Ciblage par Segment Utilisateur
Activez des fonctionnalités pour des segments d’utilisateurs spécifiques :
module FeatureFlags
def enabled_for?(flag_name, user)
return true if user_in_allowlist?(flag_name, user)
return true if segment_enabled?(flag_name, user)
return enabled_by_percentage?(flag_name, user)
end
private
def user_in_allowlist?(flag_name, user)
allowlist = Rails.app.creds.option(:features, :"#{flag_name}_users", default: [])
allowlist.include?(user.id) || allowlist.include?(user.email)
end
def segment_enabled?(flag_name, user)
segments = Rails.app.creds.option(:features, :"#{flag_name}_segments", default: [])
segments.any? { |segment| user_in_segment?(user, segment) }
end
def user_in_segment?(user, segment)
case segment.to_sym
when :staff then user.staff?
when :beta then user.beta_tester?
when :premium then user.premium?
else false
end
end
end
# credentials.yml.enc
features:
new_checkout: false
new_checkout_users:
- 123 # User ID
- "[email protected]" # Ou email
new_checkout_segments:
- staff
- beta
new_checkout_percentage: 5 # Plus 5% de tous les autres
Stratégie de déploiement : staff d’abord, puis beta testeurs, puis 5% de tout le monde, puis augmenter le pourcentage à mesure que la confiance grandit.
L’Implémentation Complète
Voici tout en un seul endroit :
# app/models/concerns/feature_flags.rb (46 lignes)
module FeatureFlags
extend self
def enabled?(flag_name)
cast_boolean(Rails.app.creds.option(:features, flag_name, default: false))
end
def disabled?(flag_name) = !enabled?(flag_name)
def enabled_for?(flag_name, user)
return true if allowlisted?(flag_name, user)
return true if segment_match?(flag_name, user)
return percentage_match?(flag_name, user) if rollout_percentage(flag_name)
enabled?(flag_name)
end
def enable!(flag_name)
(Current.feature_overrides ||= {})[flag_name] = true
end
def disable!(flag_name)
(Current.feature_overrides ||= {})[flag_name] = false
end
def with(flag_name, value)
old = Current.feature_overrides&.dig(flag_name)
value ? enable!(flag_name) : disable!(flag_name)
yield
ensure
old.nil? ? Current.feature_overrides&.delete(flag_name) : Current.feature_overrides[flag_name] = old
end
private
def cast_boolean(value) = ActiveModel::Type::Boolean.new.cast(value)
def creds_option(flag, suffix) = Rails.app.creds.option(:features, :"#{flag}#{suffix}", default: nil)
def rollout_percentage(flag) = creds_option(flag, :_percentage)&.to_i
def allowlist(flag) = creds_option(flag, :_users) || []
def segments(flag) = creds_option(flag, :_segments) || []
def allowlisted?(flag, user)
list = allowlist(flag)
list.include?(user.id) || list.include?(user.email)
end
def segment_match?(flag, user)
segments(flag).any? { |s| user.try(:"#{s}?") }
end
def percentage_match?(flag, user)
Digest::MD5.hexdigest("#{flag}-#{user.id}").first(8).to_i(16) % 100 < rollout_percentage(flag)
end
end
Pourquoi Ceci Bat une Solution SaaS
| Fonctionnalité | Cette approche | SaaS |
|---|---|---|
| Coût | Gratuit | $20-500/mois |
| Latence | Zéro (en mémoire) | Appel API |
| Contrôle de version | Oui (credentials) | Généralement non |
| Revue de code | Oui (PRs) | Dépend |
| Fonctionne hors ligne | Oui | Non |
| Complexité | Faible | Moyenne-Haute |
| Ciblage utilisateur | Basique | Avancé |
| Analytics | DIY | Intégré |
Pour la plupart des apps, vous n’avez pas besoin de mises à jour de flags en temps réel ni de règles de ciblage complexes. Vous avez besoin de flags faciles à gérer, qui n’ajoutent pas de latence, et qui ne coûtent pas d’argent.
Quand Utiliser un Vrai Service de Feature Flags
Cette approche a des limites. Considérez un service dédié quand vous avez besoin de :
- Utilisateurs non techniques gérant les flags (product managers, etc.)
- Règles de ciblage complexes (géographie, type d’appareil, etc.)
- Mises à jour en temps réel sans déploiement
- Expérimentation intégrée et analytics A/B test
- Logs d’audit et fonctionnalités de conformité
Mais pour la plupart des apps Rails ? 46 lignes de code et Rails.app.creds c’est tout ce dont vous avez besoin.
Conclusion
Rails.app.creds de Rails 8.2 avec CombinedConfiguration est une base parfaite pour les feature flags :
- Stockez les défauts dans les credentials chiffrées (versionné)
- Surchargez avec ENV (pas de déploiement nécessaire)
- Surchargez par requête (test, outils admin)
- Ajoutez des déploiements par pourcentage et ciblage utilisateur au besoin
Pas de dépendances externes. Pas de latence API. Pas de facture mensuelle.
Voir le PR #56404 pour l’implémentation de Rails.app.creds.