Flipside
Flipside is a gem for managing feature flags in your Rails applications.
Features
- enable or disable features globally
- enable features for specific records (e.g. users, organizations)
- enabled features for objects responding
trueto a certain method - Setting a start and end time for when the feature is active
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add flipside
If bundler is not being used to manage dependencies, install the gem by executing:
$ gem install flipside
Then run
$ rails generate flipside:install
This will create a migration file. Run the migration, to add the flipside tables
$ rails db:migrate
Usage
Defining Features
Features are created by running this (in a console or from code):
Flipside.create(
name: "MyFeature",
description: "Some optional description about what this feature do"
)By default features are turned off. If we would like it turned on from the get go we could pass in enabled: true.
Flipside.create(name: "MyFeature", enabled: true)Features can be active during a given period. Set activated_at and/or deactivated_at to define this period.
Note: A feature is always disabled outside of the active period.
Note: A nil value means that its active. I.e. activated_at = nil means active from the start, deactivated_at = nil means never deactivates.
Flipside.create(
name: "MyFeature",
activated_at: 1.week.from_now,
deactivated_at: 2.weeks.from_now
)Checking Feature Status
Globally
Check if a feature is enabled globally:
Flipside.enabled? "MyFeature"We can also check if a feature is disabled:
Flipside.disabled? "MyFeature"For a Specific Record
Check if a feature is enabled for a specific record (e.g. a user):
Flipside.enabled? "MyFeature", userWe can also check multiple records. .enabled? will return true if any of them have the feature enabled:
Flipside.enabled? "MyFeature", user, company, locationEnabling features for specific records
Features can be enabled for a certain record, typically a certain user or organization. These records are called entities. To enable a feature for a given record use .add_entity:
user = User.first
Flipside.enabled? "MyFeature", user # => false
Flipside.add_entity(name: "MyFeature", entity: user)
Flipside.enabled? "MyFeature", user # => trueEntities reference their record polymorphically, so there is no foreign key to clean them up when the record is deleted. A model registered as an entity (see Registering from the model) removes its entities when a record is destroyed.
Entities left behind by records deleted without callbacks (or before the concern was added) can be removed with:
Flipside.prune_orphaned_entities # => number of entities deletedFeatures can be enabled for records responding true to a certain method. This is called a "role". Given that User records have an admin? method. A feature can then be enabled
for all users who are admins, using the .add_role method:
user1 = User.new(admin: false)
user2 = User.new(admin: true)
Flipside.add_role(
name: "MyFeature",
class_name: "User",
method_name: :admin?
)
Flipside.enabled? "MyFeature", user1 # => false
Flipside.enabled? "MyFeature", user2 # => true
Flipside.enabled? "MyFeature", user1, user2 # => true, enabled for at least one.UI
Flipside comes with a Roda web ui to mange feature flags. To mount this roda app in Rails add the following to your routes.rb file.
mount Flipside::Web, at: '/flipside'Note: you probably want to wrap this inside a constraints block to provide some authentication.
Configuration
Flipside can be configured by calling some class methods on Flipside (see below).
ui_back_path is used to set a path to return to from the Flipside UI. If this is set, then the UI shows a "Back" button,
targeting this path/url. By default no back button is shown.
If create_missing_features is set to true, then features will automatically be created, whenever a check for an unknown feature is done.
This can be a convenient way of makes features "show up" in the Flipside UI. However, the description will not reveal much about what this features does.
(it will simply point to the place in the code where this check was done). So these features should be manually updated with a better description.
By default, features are not added and code like Flipside.enabled? "Some unknown feature" will simply return false.
default_object can be used to avoid the need to always pass in a record when checking if a feature is enabled. For example, say that we
always want to check if a feature is enabled for the currently logged in user (Current.user). Then we might add this configuration:
Flipside.default_object = -> { Current.user }Now we can check if a feature is enabled without needing to pass in Current.user:
# With `default_object` set then all we need is this
Flipside.enabled? :some_feature
# Which will be the same as
Flipside.enabled? :some_feature, Current.user
# If `default_object` is an Array, then each element is checked if the feature is enabled.
```ruby
Flipside.default_object = -> { [Current.user, Current.organization] }Note: if we do pass in an argument, then default_object will not be used:
Flipside.enabled? :some_feature, Current.company # check current company instead of user.
Typically this configuration should be declared in an initializer file.
```ruby
# config/initializers/flipside.rb
require 'flipside'
Flipside.ui_back_path = "/"
Flipside.create_missing_features = true
Flipside.default_object = -> { Current.user }
Registering from the model
Entities and roles are searched for in the UI, so Flipside needs to know which classes can be used. A model registers itself by including Flipside::Flippable and calling the entity macros (flipside_entity, flipside_search_by, flipside_display_as, flipside_identified_by) and/or flipside_role:
class User < ApplicationRecord
include Flipside::Flippable
flipside_search_by :name
flipside_display_as :name
flipside_role :admin?
flipside_role :awesome?, display_as: "Awesome users"
endList the class names in an initializer, so that Flipside can load them when the UI needs them (also when the app does not eager load):
# config/initializers/flipside.rb
Flipside.flippables = %w[User]A class using these macros must be listed in Flipside.flippables, and a listed class must call at least one of them, otherwise Flipside raises.
Entities
Entities can be added to a feature by searching for records.
Each class that may be used as a feature enabler is registered as an entity by calling flipside_entity, flipside_search_by, flipside_display_as or flipside_identified_by (any combination of them, in any order). flipside_entity is only needed to register with the defaults. A registered entity class also removes the entities of a record when it is destroyed. Note that this includes soft deletion that runs destroy callbacks (e.g. paranoia), so a restored record comes back without its features. Soft deletion that skips them (e.g. discard) keeps the entities.
class User < ApplicationRecord
include Flipside::Flippable
flipside_entity
endflipside_identified_by sets the column Flipside uses to identify records. This defaults to :id and typically does not need to be changed.
Currently composite keys are not supported.
flipside_identified_by :uuidflipside_search_by dictates how records are found from searching in the ui. It takes a block, which is called with the search string in the context of the class and is expected to return an object responding to to_a (e.g. an AR collection).
This gives us the flexibility to decide how to search for entities. For example, to search for users with matching first name or last name or an email
starting with query, something like this could be used.
flipside_search_by do |str|
where(
"lower(first_name) = :name OR lower(last_name) = :name OR email LIKE :str",
name: str.downcase,
str: "#{str}%"
)
endInstead of a block, it may be given the name of a column, e.g. :name. Then records with an exact match on that column are returned, i.e. User.where(name: query):
flipside_search_by :nameWithout flipside_search_by, records are found by an exact match on identified_by.
flipside_display_as configures how these entities show up in the combobox. When given the name of an instance method, that method is called on the entity.
For example, given the following setup. Users will be displayed with first name and last name:
class User < ApplicationRecord
include Flipside::Flippable
flipside_display_as :name
def name
[first_name, last_name].compact.map(&:capitalize).join(" ")
end
endWhen given a block, it is called with the entity and is expected to return a string used for displaying the entity. The config above could then instead be done using:
flipside_display_as do |user|
[user.first_name, user.last_name].compact.map(&:capitalize).join(" ")
endWithout flipside_display_as, entities are displayed by their identified_by value.
An STI subclass inherits these settings. When a subclass calls one of the macros itself, it is registered as an entity of its own, so it must also be listed in Flipside.flippables and shows up separately in the UI.
The search_by, display_as and identified_by keyword arguments of flipside_entity from Flipside 0.4.0 are deprecated in favour of these macros, and will be removed in 1.0.
Roles
Features can be enabled for certain roles, by searching for roles (by method name).
This is configured by calling flipside_role for each role to be added. A role consists of a class and a corresponding instance method, and display_as optionally sets how it shows up in the UI.
class User < ApplicationRecord
include Flipside::Flippable
flipside_role :admin?
flipside_role :awesome?, display_as: "Awesome users"
endClasses you cannot edit
A class from another gem can be registered by including the concern from to_prepare, which runs at boot and again after every reload. It must still be listed in Flipside.flippables:
# config/initializers/flipside.rb
Flipside.flippables = %w[User OtherGem::Account]
Rails.application.config.to_prepare do
OtherGem::Account.include(Flipside::Flippable)
OtherGem::Account.flipside_display_as(:name)
endUpgrading from register_entity and register_role
Flipside.register_entity and Flipside.register_role are deprecated and will be removed in Flipside 1.0. Move each call into its model:
# Before, in config/initializers/flipside.rb
Flipside.register_entity(class_name: "User", search_by: :name, display_as: :name)
Flipside.register_role(class_name: "User", method_name: :admin?)
# After, in config/initializers/flipside.rb
Flipside.flippables = %w[User]
# and in app/models/user.rb
class User < ApplicationRecord
include Flipside::Flippable
flipside_search_by :name
flipside_display_as :name
flipside_role :admin?
endIn a Rails 7.1+ app the deprecation warnings follow the app's own deprecation settings (Rails.application.deprecators[:flipside]).
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rspec to run the tests.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/apoex/flipside.
License
The gem is available as open source under the terms of the MIT License.


