Build Status

Flipflop your features

Flipflop provides a declarative, layered way of enabling and disabling application functionality at run-time. It is originally based on Flip. Flipflop has the following features:

You can configure strategy layers that will evaluate if a feature is currently enabled or disabled. Available strategies are:

Flipflop has a dashboard interface that's easy to understand and use.

Dashboard

If you prefer, you can use the included rake tasks to enable or disable features.

rake flipflop:features                    # Shows features table
rake flipflop:turn_on[feature,strategy]   # Enables a feature with the specified strategy
rake flipflop:turn_off[feature,strategy]  # Disables a feature with the specified strategy
rake flipflop:clear[feature,strategy]     # Clears a feature with the specified strategy

Rails requirements

This gem requires Rails 4, 5, 6 or 7. Using an ORM layer is entirely optional.

Installation

Add the gem to your Gemfile:

gem "flipflop"

Generate routes, feature settings and database migration:

rails g flipflop:install

Run the migration to store feature settings in your database:

rake db:migrate

Declaring features

Features and strategies are declared in config/features.rb:

Flipflop.configure do
  # Strategies will be used in the order listed here.
  strategy :cookie
  strategy :active_record # or :sequel, :redis
  strategy :default

  # Basic feature declaration:
  feature :shiny_things

  # Enable features by default:
  feature :world_domination, default: true

  # Group features together:
  group :improved_design do
    feature :improved_navigation
    feature :improved_homepage
  end
end

This file is automatically reloaded in development mode. No need to restart your server after making changes.

Feature definitions support these options:

Strategies

The following strategies are provided:

All strategies support these options, to change the appearance of the dashboard:

The same strategy type can be used multiple times, as long as the options are different. To prevent subtle bugs, an error is raised if two identical strategies are configured.

Checking if a feature is enabled

Flipflop.enabled? or the dynamic predicate methods can be used to check feature state:

Flipflop.enabled?(:world_domination)  # true
Flipflop.world_domination?            # true

Flipflop.enabled?(:shiny_things)      # false
Flipflop.shiny_things?                # false

This works everywhere. In your views:

<div>
  <% if Flipflop.world_domination? %>
    <%= link_to "Dominate World", world_dominations_path %>
  <% end %>
</div>

In your controllers:

class ShinyThingsController < ApplicationController
  def index
    return head :forbidden unless Flipflop.shiny_things?
    # Proceed with shiny things...
  end
end

In your models:

class ShinyThing < ActiveRecord::Base
  after_initialize do
    if !Flipflop.shiny_things?
      raise ActiveRecord::RecordNotFound
    end
  end
end

Custom strategies

Custom light-weight strategies can be defined with a block:

Flipflop.configure do
  strategy :random do |feature|
    rand(2).zero?
  end
  # ...
end

You can define your own custom strategies by inheriting from Flipflop::Strategies::AbstractStrategy:

class UserPreferenceStrategy < Flipflop::Strategies::AbstractStrategy
  class << self
    def default_description
      "Allows configuration of features per user."
    end
  end

  def switchable?
    # Can only switch features on/off if we have the user's session.
    # The `request` method is provided by AbstractStrategy.
    request?
  end

  def enabled?(feature)
    # Can only check features if we have the user's session.
    return unless request?
    find_current_user.enabled_features[feature]
  end

  def switch!(feature, enabled)
    user = find_current_user
    user.enabled_features[feature] = enabled
    user.save!
  end

  def clear!(feature)
    user = find_current_user
    user.enabled_features.delete(feature)
    user.save!
  end

  private

  def find_current_user
    # The `request` method is provided by AbstractStrategy.
    User.find_by_id(request.session[:user_id])
  end
end

Use it in config/features.rb:

Flipflop.configure do
  strategy UserPreferenceStrategy # name: "my strategy", description: "..."
end

If you define your class inside Flipflop::Strategies, you can use the shorthand name to refer to your strategy:

module Flipflop::Strategies
  class UserPreferenceStrategy < AbstractStrategy
    # ...
  end
end
Flipflop.configure do
  strategy :user_preference
end

Dashboard access control

The dashboard provides visibility and control over the features.

You don't want the dashboard to be public. For that reason it is only available in the development and test environments by default. Here's one way of implementing access control.

In app/config/application.rb:

config.flipflop.dashboard_access_filter = :require_authenticated_user

In app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  def require_authenticated_user
    head :forbidden unless User.logged_in?
  end
end

Or directly in app/config/application.rb:

config.flipflop.dashboard_access_filter = -> {
  head :forbidden unless User.logged_in?
}

Features in Rails engines

You can use features in Rails engines. Simply tell Flipflop to load files from an additional file in an initializer. You can define features and strategies. Both will be merged with application features. You'll have to somewhat careful with defining strategies in the engine to avoid conflicts.

class MyEngine < Rails::Engine
  initializer "load_features" do
    # Features from config/features.rb in your engine are merged with
    # any application features.
    Flipflop::FeatureLoader.current.append(self)
  end
end

Internationalization

The dashboard is translatable. Make sure I18n.locale is set to the correct value in your ApplicationController or alternatively in dashboard_access_filter.

Take a look at the English translations to see which keys should be present and translated in your locale file.

Testing

In your test environment, you typically want to keep your features. But to make testing easier, you may not want to use any of the strategies you use in development and production. You can replace all strategies with a single :test strategy by calling Flipflop::FeatureSet.current.test!. The test strategy will be returned. You can use this strategy to enable and disable features.

describe WorldDomination do
  before do
    test_strategy = Flipflop::FeatureSet.current.test!
    test_strategy.switch!(:world_domination, true)
  end

  it "should dominate the world" do
     # ...
  end
end

If you are not happy with the default test strategy (which is essentially a simple thread-safe hash object), you can provide your own implementation as argument to the test! method.

License

This software is licensed under the MIT License. View the license.

Join libs.tech

...and unlock some superpowers

GitHub

We won't share your data with anyone else.