feat: Add fun_with_flags to the system
This commit is contained in:
parent
36ad2d6e95
commit
6a6f9efd58
8 changed files with 96 additions and 0 deletions
|
@ -57,6 +57,7 @@ defmodule AppWeb.Router do
|
|||
pow_extension_routes()
|
||||
end
|
||||
|
||||
use Legendary.Core.Routes
|
||||
use Legendary.Admin.Routes
|
||||
use Legendary.Content.Routes
|
||||
end
|
||||
|
|
32
apps/core/guides/features/feature-flags.md
Normal file
32
apps/core/guides/features/feature-flags.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Feature Flags
|
||||
|
||||
Legendary comes with [Fun with Flags](https://github.com/tompave/fun_with_flags)
|
||||
preconfigured for managing [feature flags](https://en.wikipedia.org/wiki/Feature_toggle).
|
||||
This allows you to have more granular control over which users see which features
|
||||
and when. For example, you can hide a feature which is not complete, or show it
|
||||
to only a select group of testers.
|
||||
|
||||
|
||||
Fun With Flags supports a variety of different feature gate types. From
|
||||
the Fun With Flags docs:
|
||||
|
||||
* **Boolean**: globally on and off.
|
||||
* **Actors**: on or off for specific structs or data. The `FunWithFlags.Actor` protocol can be implemented for types and structs that should have specific rules. For example, in web applications it's common to use a `%User{}` struct or equivalent as an actor, or perhaps the current country of the request.
|
||||
* **Groups**: on or off for structs or data that belong to a category or satisfy a condition. The `FunWithFlags.Group` protocol can be implemented for types and structs that belong to groups for which a feature flag can be enabled or disabled. For example, one could implement the protocol for a `%User{}` struct to identify administrators.
|
||||
* **%-of-Time**: globally on for a percentage of the time. It ignores actors and groups. Mutually exclusive with the %-of-actors gate.
|
||||
* **%-of-Actors**: globally on for a percentage of the actors. It only applies when the flag is checked with a specific actor and is ignored when the flag is checked without actor arguments. Mutually exclusive with the %-of-time gate.
|
||||
|
||||
Since feature flags may be checked often (sometimes multiple times per request),
|
||||
Fun With Flags uses a two-layer approach. Flags are cached in [ETS](https://erlang.org/doc/man/ets.html)
|
||||
and also persisted to longer-term storage so that they are not lost when the app
|
||||
restarts.
|
||||
|
||||
By default, Legendary caches the flags for five minutes. We use Ecto for
|
||||
persistence. We also use Phoenix PubSub to inform application nodes when a flag
|
||||
has been updated. This configuration is a sensible default that we would not
|
||||
expect you to need to change in most cases.
|
||||
|
||||
## UI
|
||||
|
||||
We integrate the Fun With Flags UI for managing flags. You can reach it through
|
||||
a link in the admin.
|
|
@ -3,6 +3,12 @@ defmodule Legendary.Auth.UserAdmin do
|
|||
alias Legendary.Auth.User
|
||||
alias Legendary.Core.Repo
|
||||
|
||||
def custom_links(_schema) do
|
||||
[
|
||||
%{name: "Feature Flags", url: "/admin/feature-flags", order: 2, location: :top, icon: "flag"},
|
||||
]
|
||||
end
|
||||
|
||||
def create_changeset(schema, attrs) do
|
||||
Legendary.Auth.User.admin_changeset(schema, attrs)
|
||||
end
|
||||
|
|
11
apps/core/lib/core_web/routes.ex
Normal file
11
apps/core/lib/core_web/routes.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule Legendary.Core.Routes do
|
||||
defmacro __using__(_opts \\ []) do
|
||||
quote do
|
||||
scope path: "/admin/feature-flags" do
|
||||
pipe_through :require_admin
|
||||
|
||||
forward "/", FunWithFlags.UI.Router, namespace: "admin/feature-flags"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -58,6 +58,7 @@ defmodule Legendary.Core.MixProject do
|
|||
"guides/features/content-management.md",
|
||||
"guides/features/devops-templates.md",
|
||||
"guides/features/email.md",
|
||||
"guides/features/feature-flags.md",
|
||||
"guides/features/i18n.md",
|
||||
"guides/features/tasks-and-scripts.md",
|
||||
]
|
||||
|
@ -139,6 +140,8 @@ defmodule Legendary.Core.MixProject do
|
|||
{:ex_cldr, "~> 2.13.0"},
|
||||
{:ex_doc, "~> 0.24", only: :dev, runtime: false},
|
||||
{:excoveralls, "~> 0.10", only: [:dev, :test]},
|
||||
{:fun_with_flags, "~> 1.6.0"},
|
||||
{:fun_with_flags_ui, "~> 0.7.2"},
|
||||
{:phoenix, "~> 1.5.8"},
|
||||
{:phoenix_ecto, "~> 4.1"},
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
defmodule Legendary.Core.Repo.Migrations.AddFeatureFlagTable do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
create table(:fun_with_flags_toggles, primary_key: false) do
|
||||
add :id, :bigserial, primary_key: true
|
||||
add :flag_name, :string, null: false
|
||||
add :gate_type, :string, null: false
|
||||
add :target, :string, null: false
|
||||
add :enabled, :boolean, null: false
|
||||
end
|
||||
|
||||
create index(
|
||||
:fun_with_flags_toggles,
|
||||
[:flag_name, :gate_type, :target],
|
||||
[unique: true, name: "fwf_flag_name_gate_target_idx"]
|
||||
)
|
||||
end
|
||||
|
||||
def down do
|
||||
drop table(:fun_with_flags_toggles)
|
||||
end
|
||||
end
|
|
@ -76,6 +76,24 @@ config :app,
|
|||
|
||||
config :mnesia, dir: to_charlist(Path.expand("./priv/mnesia"))
|
||||
|
||||
# Feature flags
|
||||
|
||||
config :fun_with_flags, :cache,
|
||||
enabled: true,
|
||||
ttl: 300 # seconds
|
||||
|
||||
config :fun_with_flags, :persistence,
|
||||
adapter: FunWithFlags.Store.Persistent.Ecto,
|
||||
repo: Legendary.Core.Repo
|
||||
|
||||
config :fun_with_flags, :cache_bust_notifications,
|
||||
enabled: true,
|
||||
adapter: FunWithFlags.Notifications.PhoenixPubSub,
|
||||
client: App.PubSub
|
||||
|
||||
# Notifications can also be disabled, which will also remove the Redis/Redix dependency
|
||||
config :fun_with_flags, :cache_bust_notifications, [enabled: false]
|
||||
|
||||
import_config "email_styles.exs"
|
||||
import_config "admin.exs"
|
||||
|
||||
|
|
2
mix.lock
2
mix.lock
|
@ -28,6 +28,8 @@
|
|||
"excoveralls": {:hex, :excoveralls, "0.13.0", "4e1b7cc4e0351d8d16e9be21b0345a7e165798ee5319c7800b9138ce17e0b38e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "fe2a56c8909564e2e6764765878d7d5e141f2af3bc8ff3b018a68ee2a218fced"},
|
||||
"file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"},
|
||||
"floki": {:hex, :floki, "0.25.0", "b1c9ddf5f32a3a90b43b76f3386ca054325dc2478af020e87b5111c19f2284ac", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "631f4e627c46d5ecd347df5a2accdaf0621c77c3693c5b75a8ad58e84c61f242"},
|
||||
"fun_with_flags": {:hex, :fun_with_flags, "1.6.0", "507fcbc19374e83d34ba13d63b0816d37af952da1c6592978bbf40dad6a2e671", [:mix], [{:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:redix, "~> 1.0", [hex: :redix, repo: "hexpm", optional: true]}], "hexpm", "96b53f54737906c5a83b5d89922ca1145fda2d50db23b1f619478460b8d5f8d8"},
|
||||
"fun_with_flags_ui": {:hex, :fun_with_flags_ui, "0.7.2", "c8df9e90f92481c014824ab1ff5db7d501ac34ec28a4599b76251ec5a6db0861", [:mix], [{:cowboy, ">= 1.0.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:fun_with_flags, "~> 1.1", [hex: :fun_with_flags, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "99b3635b067304722560ea3d5aab38ae23ea1217366c95ecb6263559488a22f2"},
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
|
||||
"gen_stage": {:hex, :gen_stage, "1.0.0", "51c8ae56ff54f9a2a604ca583798c210ad245f415115453b773b621c49776df5", [:mix], [], "hexpm", "1d9fc978db5305ac54e6f5fec7adf80cd893b1000cf78271564c516aa2af7706"},
|
||||
"gen_state_machine": {:hex, :gen_state_machine, "2.1.0", "a38b0e53fad812d29ec149f0d354da5d1bc0d7222c3711f3a0bd5aa608b42992", [:mix], [], "hexpm", "ae367038808db25cee2f2c4b8d0531522ea587c4995eb6f96ee73410a60fa06b"},
|
||||
|
|
Loading…
Reference in a new issue