defmodule Unleash do
@moduledoc """
If you have no plans on extending the client, then `Unleash` will be the main
usage point of the library. Upon starting your app, the client is registered
with the unleash server, and two `GenServer`s are started, one to fetch and
poll for feature flags from the server, and one to send metrics.
Configuring `:disable_client` to `true` disables both servers as well as
registration, while configuring `:disable_metrics` to `true` disables only
the metrics `GenServer`.
"""
use Application
alias Unleash.Config
alias Unleash.Feature
alias Unleash.Metrics
alias Unleash.Repo
alias Unleash.Variant
@typedoc """
The context needed for a few activation strategies. Check their documentation
for the required key.
* `:user_id` is the ID of the user interacting _with your system_, can be any
`String.t()`
* `session_id` is the ID of the current session _in your system_, can be any
`String.t()`
* `remote_address` is the address of the user interacting _with your system_,
can be any `String.t()`
"""
@type context :: %{
user_id: String.t(),
session_id: String.t(),
remote_address: String.t()
}
@doc """
Aliased to `enabled?/2`
"""
@spec is_enabled?(atom() | String.t(), boolean) :: boolean
def is_enabled?(feature, default) when is_boolean(default),
do: enabled?(feature, default)
@doc """
Aliased to `enabled?/3`
"""
@spec is_enabled?(atom() | String.t(), map(), boolean) :: boolean
def is_enabled?(feature, context \\ %{}, default \\ false),
do: enabled?(feature, context, default)
@doc """
Checks if the given feature is enabled. Checks as though an empty context was
passed in.
## Examples
iex> Unleash.enabled?(:my_feature, false)
false
iex> Unleash.enabled?(:my_feature, true)
true
"""
@spec enabled?(atom() | String.t(), boolean) :: boolean
def enabled?(feature, default) when is_boolean(default),
do: enabled?(feature, %{}, default)
@doc """
Checks if the given feature is enabled.
If `:disable_client` is `true`, simply returns the given `default`.
If `:disable_metrics` is `true`, nothing is logged about the given toggle.
## Examples
iex> Unleash.enabled?(:my_feature)
false
iex> Unleash.enabled?(:my_feature, context)
false
iex> Unleash.enabled?(:my_feature, context, true)
false
"""
@spec enabled?(atom() | String.t(), map(), boolean) :: boolean
def enabled?(feature, context \\ %{}, default \\ false) do
start_metadata = Unleash.Client.telemetry_metadata(%{feature: feature})
:telemetry.span(
[:unleash, :feature, :enabled?],
start_metadata,
fn ->
{result, metadata} =
if Config.disable_client() do
{default, %{reason: :disabled_client}}
else
feature
|> Repo.get_feature()
|> case do
nil ->
{default, %{reason: :feature_not_found}}
feature ->
{result, strategy_evaluations} =
Feature.enabled?(feature, Map.put(context, :feature_toggle, feature.name))
Metrics.add_metric({feature, result})
metadata = %{
reason: :strategy_evaluations,
strategy_evaluations: strategy_evaluations,
feature_enabled: feature.enabled
}
{result, metadata}
end
end
telemetry_metadata =
start_metadata
|> Map.merge(metadata)
|> Map.put(:result, result)
{result, telemetry_metadata}
end
)
end
@doc """
Returns a variant for the given name.
If `:disable_client` is `true`, returns the fallback.
A [variant](https://unleash.github.io/docs/beta_features#feature-toggle-variants)
allows for more complicated toggling than a simple `true`/`false`, instead
returning one of the configured variants depending on whether or not there
are any overrides for a given context value as well as factoring in the
weights for the various weight options.
## Examples
iex> Unleash.get_variant(:test)
%{enabled: true, name: "test", payload: %{...}}
iex> Unleash.get_variant(:test)
%{enabled: false, name: "disabled"}
"""
@spec get_variant(atom() | String.t(), map(), Variant.result()) :: Variant.result()
def get_variant(name, context \\ %{}, fallback \\ Variant.disabled()) do
start_metadata = Unleash.Client.telemetry_metadata(%{variant: name})
:telemetry.span(
[:unleash, :variant, :get],
start_metadata,
fn ->
{result, metadata} =
if Config.disable_client() do
{fallback, %{reason: :disabled_client}}
else
name
|> Repo.get_feature()
|> case do
nil ->
{fallback, %{reason: :feature_not_found}}
feature ->
Variant.select_variant(feature, context)
end
end
{result, Map.merge(start_metadata, metadata)}
end
)
end
@doc false
def start(_type, _args) do
children =
[
{Repo, Config.disable_client()},
{{Metrics, name: Metrics}, Config.disable_client() or Config.disable_metrics()}
]
|> Enum.filter(fn {_m, not_enabled} -> not not_enabled end)
|> Enum.map(fn {module, _e} -> module end)
unless children == [] do
Config.client().register_client()
end
Supervisor.start_link(children, strategy: :one_for_one)
end
end