diff --git a/Gemfile b/Gemfile index 99a5fe6..ab24c7b 100644 --- a/Gemfile +++ b/Gemfile @@ -94,6 +94,11 @@ gem "countries" # Markdown parsing gem "redcarpet" +# Feature flags +gem "flipper" +gem "flipper-active_record" +gem "flipper-ui" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index ea8f9bc..307bb5a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,6 +175,18 @@ GEM ffi (>= 1.15.5) rake flamegraph (0.9.5) + flipper (1.3.4) + concurrent-ruby (< 2) + flipper-active_record (1.3.4) + activerecord (>= 4.2, < 9) + flipper (~> 1.3.4) + flipper-ui (1.3.4) + erubi (>= 1.0.0, < 2.0.0) + flipper (~> 1.3.4) + rack (>= 1.4, < 4) + rack-protection (>= 1.5.3, < 5.0.0) + rack-session (>= 1.0.2, < 3.0.0) + sanitize (< 8) fugit (1.11.1) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -339,6 +351,10 @@ GEM rack (>= 3.0.14) rack-mini-profiler (3.3.1) rack (>= 1.2.0) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) @@ -422,6 +438,9 @@ GEM ruby-progressbar (1.13.0) rubyzip (2.4.1) safely_block (0.5.0) + sanitize (7.0.0) + crass (~> 1.0.2) + nokogiri (>= 1.16.8) securerandom (0.4.1) selenium-webdriver (4.33.0) base64 (~> 0.2) @@ -533,6 +552,9 @@ DEPENDENCIES doorkeeper (~> 5.8) dotenv-rails flamegraph + flipper + flipper-active_record + flipper-ui geocoder good_job honeybadger diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb new file mode 100644 index 0000000..da80cad --- /dev/null +++ b/config/initializers/flipper.rb @@ -0,0 +1,43 @@ +Rails.application.configure do + ## Memoization ensures that only one adapter call is made per feature per request. + ## For more info, see https://www.flippercloud.io/docs/optimization#memoization + # config.flipper.memoize = true + + ## Flipper preloads all features before each request, which is recommended if: + ## * you have a limited number of features (< 100?) + ## * most of your requests depend on most of your features + ## * you have limited gate data combined across all features (< 1k enabled gates, like individual actors, across all features) + ## + ## For more info, see https://www.flippercloud.io/docs/optimization#preloading + # config.flipper.preload = true + + ## Warn or raise an error if an unknown feature is checked + ## Can be set to `:warn`, `:raise`, or `false` + # config.flipper.strict = Rails.env.development? && :warn + + ## Show Flipper checks in logs + # config.flipper.log = true + + ## Reconfigure Flipper to use the Memory adapter and disable Cloud in tests + # config.flipper.test_help = true + + ## The path that Flipper Cloud will use to sync features + # config.flipper.cloud_path = "_flipper" + + ## The instrumenter that Flipper will use. Defaults to ActiveSupport::Notifications. + # config.flipper.instrumenter = ActiveSupport::Notifications +end + +Flipper.configure do |config| + config.use Flipper::Adapters::ActiveRecord +end + +## Register a group that can be used for enabling features. +## +## Flipper.enable_group :my_feature, :admins +## +## See https://www.flippercloud.io/docs/features#enablement-group + +Flipper.register(:admins) do |actor| + actor.respond_to?(:admin?) && actor.admin? +end diff --git a/config/routes.rb b/config/routes.rb index 5afde3a..442fa06 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,7 @@ Rails.application.routes.draw do constraints AdminConstraint do mount GoodJob::Engine => "good_job" mount AhoyCaptain::Engine => "/ahoy_captain" + mount Flipper::UI.app(Flipper) => "flipper" get "/impersonate/:id", to: "sessions#impersonate", as: :impersonate_user end diff --git a/db/migrate/20250611061502_add_beta_features_to_users.rb b/db/migrate/20250611061502_add_beta_features_to_users.rb new file mode 100644 index 0000000..60caddc --- /dev/null +++ b/db/migrate/20250611061502_add_beta_features_to_users.rb @@ -0,0 +1,5 @@ +class AddBetaFeaturesToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :beta_features, :text, default: '[]' + end +end diff --git a/db/migrate/20250611062306_create_flipper_tables.rb b/db/migrate/20250611062306_create_flipper_tables.rb new file mode 100644 index 0000000..a9d6d1d --- /dev/null +++ b/db/migrate/20250611062306_create_flipper_tables.rb @@ -0,0 +1,22 @@ +class CreateFlipperTables < ActiveRecord::Migration[8.0] + def up + create_table :flipper_features do |t| + t.string :key, null: false + t.timestamps null: false + end + add_index :flipper_features, :key, unique: true + + create_table :flipper_gates do |t| + t.string :feature_key, null: false + t.string :key, null: false + t.text :value + t.timestamps null: false + end + add_index :flipper_gates, [ :feature_key, :key, :value ], unique: true, length: { value: 255 } + end + + def down + drop_table :flipper_gates + drop_table :flipper_features + end +end diff --git a/db/migrate/20250611062456_remove_beta_features_from_users.rb b/db/migrate/20250611062456_remove_beta_features_from_users.rb new file mode 100644 index 0000000..f38ee31 --- /dev/null +++ b/db/migrate/20250611062456_remove_beta_features_from_users.rb @@ -0,0 +1,5 @@ +class RemoveBetaFeaturesFromUsers < ActiveRecord::Migration[8.0] + def change + remove_column :users, :beta_features, :text + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c5b14a..a0f68e8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_06_09_203830) do +ActiveRecord::Schema[8.0].define(version: 2025_06_11_062456) do create_schema "pganalyze" # These are extensions that must be enabled in order to support this database @@ -107,6 +107,22 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_09_203830) do t.index ["user_id"], name: "index_email_verification_requests_on_user_id" end + create_table "flipper_features", force: :cascade do |t| + t.string "key", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_flipper_features_on_key", unique: true + end + + create_table "flipper_gates", force: :cascade do |t| + t.string "feature_key", null: false + t.string "key", null: false + t.text "value" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true + end + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false