January 22, 2012

Simple Feature Flags for Rails + Mongoid

Selectively rolling out new bits of an app using feature flags isn’t exactly a new idea. But being reminded of it during the keynote preceding the overall awesomeness of Heroku’s developer conference last week, right in the midst of my work adding more and more faster and faster to Fullscreen’s¬†products, made me think that it was high time we got something like this hooked up.

Like any properly lazy developer, the first thing I did was search for a gem that could do this shit for me. And there were a few. And some looked pretty good. But the basic use cases for this feature were totally simple and concrete.

1. Enable and disable a feature on a per-user basis
2. Enable and disable a feature globally

And since I was looking for an easy feature to start learning TDD on (a post for another time) I figured I’d do it myself. The implementation is as simple as keeping track of a hash for each user that holds the names of whatever features they’ve had enabled.

Our app runs on Rails 3/MongoDB. Like pretty much every app I’ve ever worked on ever, there’s a User model that enables authentication and a whole lot more. I add a field to our User model to keep track of enabled features in a hash:

  field :feature_flags, :type => Hash, :default => {}

Because we’re using Mongo it doesn’t matter that, as we start to enable features, half of our user instances might have contents in this field and half might have nothing. Speaking of which, I need to provide a way to enable and disable a feature for a single user:

  def enable_feature(name)
    self.feature_flags[name] = true
    self.save!
  end

  def disable_feature(name)
    self.feature_flags[name] = false
    self.save!
  end

So now I can enable a new feature for a single user by calling user.enable_feature("fireside_romance"). I can also disable that feature for that same user with user.disable_feature("fireside_romance"). Disabling a feature sets the value to false instead of removing it from the array because we might want to know which users have had a feature enabled previously. That kickass Fireside Romance feature might break so we can disable it, fix the bug, and then re-enable it only for that sexy group of users who had it enabled in the past.

On the other hand, we’re definitely not going to want to enable features one-by-one for thousands of users, so we need a way to enable and disable a feature for the entire user base which I implemented as a pair of User class methods.

  def self.enable_feature_globally(name)
    User.all.each do |u|
      u.enable_feature(name)
    end
  end

  def self.disable_feature_globally(name)
    User.all.each do |u|
      u.disable_feature(name)
    end
  end

And now that we’re able to do all this, we need some way to see what’s actually enabled for a given user.

  def feature_enabled?(name)
    !self.feature_flags[name].blank? ? self.feature_flags[name] : false
  end

  def feature_added?(name)
    !self.feature_flags[name].nil? ? true : false
  end

So calling current_user.feature_enabled?("bearskin_rug") will tell me simply whether we’ve enabled the Bearskin Rug for the current user. And, though less immediately useful, calling current_user.feature_added?("bearskin_rug") will let me know whether the flag for Bearskin Rug exists at all; if this returns false, this user’s never seen a Bearskin Rug before and I can display messaging and/or functionality to that user accordingly.

Putting all of this into practice: now I can set up a code path in my view (or anywhere else, really) like the following, which shows a feature only to the users who’ve got it explicitly enabled.

- if current_user.feature_enabled?('waterbed')
  // render the 'invite a friend' form
- else
  // display the 'add waterbed' button

Right now we’re going to have to enable select users manually through the Rails console, but eventually we’ll build a basic admin view, kinda like Flickr’s internal Feature Flip page.

If you want to see how it comes together in a sample User model, here’s a gist which does just that (and really nothing more).