Add feature flag support to Pages (#8149)

* Add 'feature_flag' field to pages table, form

* Add 'exist?' to FeatureFlag

* Test for feature flag when loading custom pages

* Refactor FeatureFlag.enabled? to check both boolean and user gates

* Ugly test fixes

* PR feedback and refactoring

* PR refactor: don't require database table!
This commit is contained in:
Josh Puetz 2020-06-01 09:23:19 -05:00 committed by GitHub
parent 3233558c5d
commit f165c47b34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 6 deletions

View file

@ -4,6 +4,8 @@ class PagesController < ApplicationController
def show
@page = Page.find_by!(slug: params[:slug])
not_found unless FeatureFlag.accessible?(@page.feature_flag_name, current_user)
set_surrogate_key_header "show-page-#{params[:slug]}"
end

View file

@ -111,8 +111,12 @@ class StoriesController < ApplicationController
Honeycomb.add_field("stories_route", "org")
handle_organization_index
elsif @page
Honeycomb.add_field("stories_route", "page")
handle_page_display
if FeatureFlag.accessible?(@page.feature_flag_name, current_user)
Honeycomb.add_field("stories_route", "page")
handle_page_display
else
not_found
end
else
Honeycomb.add_field("stories_route", "user")
handle_user_index

View file

@ -1,7 +1,9 @@
module FeatureFlag
extend self # rubocop:disable Style/ModuleFunction
class << self
delegate :enabled?, :exist?, to: Flipper
def enabled?(feature_name, *args)
Flipper[feature_name].enabled?(*args)
def accessible?(feature_flag_name, *args)
feature_flag_name.blank? || !exist?(feature_flag_name) || enabled?(feature_flag_name, *args)
end
end
end

View file

@ -18,6 +18,10 @@ class Page < ApplicationRecord
is_top_level_path ? "/#{slug}" : "/page/#{slug}"
end
def feature_flag_name
"page_#{slug}"
end
private
def evaluate_markdown

View file

@ -23,7 +23,7 @@
</div>
<div class="form-group">
<% if @page.social_image_url %>
<img src="<%= @page.social_image_url%>" style="max-width:500px;display:block" />
<img src="<%= @page.social_image_url %>" style="max-width:500px;display:block" />
<% end %>
<%= form.label :social_image %>
<%= form.file_field :social_image, class: "form-control" %>
@ -38,6 +38,24 @@
<%= form.check_box :is_top_level_path %>
<p>(Determines if it is accessible by <code>/page-slug</code> vs <code>/page/page-slug</code>) Be careful! ⚠️</p>
</div>
<div class="form-group">
<p>
<b><%= link_to "Feature Flag", "/internal/feature_flags" %></b>
<span class="badge badge-<%= FeatureFlag.exist?(@page.feature_flag_name) ? "success" : "warning" %>">
<%= FeatureFlag.exist?(@page.feature_flag_name) ? "Present" : "Not Present" %>
</span>
</br>
<% if FeatureFlag.exist?(@page.feature_flag_name) %>
Access to this page is being guarded by the feature flag <code><%= @page.feature_flag_name %></code>.
<%= link_to "Modify flag here", "/internal/feature_flags/features/#{@page.feature_flag_name}" %>
<% else %>
Everyone has access. Optionally guard access to this page by creating feature <code><%= @page.feature_flag_name %></code>
<%= link_to "here", "/internal/feature_flags/features/" %>
<% end %>
</br>
</p>
</div>
<%= form.submit class: "btn btn-primary float-right" %>
<% end %>
</div>

View file

@ -0,0 +1,66 @@
require "rails_helper"
UserStruct = Struct.new(:flipper_id)
describe FeatureFlag, type: :helper do
describe ".enabled?" do
it "calls Flipper's enabled? method" do
allow(Flipper).to receive(:enabled?).with("foo")
described_class.enabled?("foo")
expect(Flipper).to have_received(:enabled?).with("foo")
end
end
describe ".exist?" do
it "calls Flipper's exist? method" do
allow(Flipper).to receive(:exist?).with("foo")
described_class.exist?("foo")
expect(Flipper).to have_received(:exist?).with("foo")
end
end
describe ".accessible?" do
let(:user) { UserStruct.new(flipper_id: 1) }
it "returns false when flag doesn't exist" do
expect(described_class.accessible?("missing_flag")).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
it "returns true when flag is empty" do
expect(described_class.accessible?("")).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
it "returns true when flag is nil" do
expect(described_class.accessible?(nil)).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
context "when flag exists and is set to off" do
before { Flipper.disable("flag") }
it "returns false" do
expect(described_class.accessible?("flag")).to be_falsy # rubocop:disable Rspec/PredicateMatcher
end
it "returns true when flag is on for user" do
Flipper.enable_actor("flag", user)
expect(described_class.accessible?("flag", user)).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
end
context "when flag exists and is set to on" do
before { Flipper.enable("flag") }
it "returns true" do
expect(described_class.accessible?("flag")).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
it "returns true for a user" do
expect(described_class.accessible?("flag", user)).to be_truthy # rubocop:disable Rspec/PredicateMatcher
end
end
end
end