Packages
ConfigCat SDK for Elixir. Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages.
Current section
Files
Jump to
Current section
Files
lib/config_cat/client.ex
defmodule ConfigCat.Client do
@moduledoc false
use GenServer
alias ConfigCat.CachePolicy
alias ConfigCat.Config
alias ConfigCat.Config.Setting
alias ConfigCat.EvaluationDetails
alias ConfigCat.EvaluationLogger
alias ConfigCat.FetchTime
alias ConfigCat.Hooks
alias ConfigCat.OverrideDataSource
alias ConfigCat.Rollout
alias ConfigCat.User
require ConfigCat.Config.SettingType, as: SettingType
require ConfigCat.ConfigCatLogger, as: ConfigCatLogger
defmodule State do
@moduledoc false
use TypedStruct
typedstruct enforce: true do
field :cache_policy, module()
field :default_user, User.t(), enforce: false
field :flag_overrides, OverrideDataSource.t()
field :instance_id, ConfigCat.instance_id()
end
@spec new(keyword()) :: t()
def new(options) do
options = Keyword.merge([cache_policy: CachePolicy], options)
struct!(__MODULE__, options)
end
@spec clear_default_user(t()) :: t()
def clear_default_user(%__MODULE__{} = state) do
%{state | default_user: nil}
end
@spec with_default_user(t(), User.t()) :: t()
def with_default_user(%__MODULE__{} = state, %User{} = user) do
%{state | default_user: user}
end
end
@type option ::
{:cache_policy, module()}
| {:default_user, User.t()}
| {:flag_overrides, OverrideDataSource.t()}
| {:instance_id, ConfigCat.instance_id()}
@type options :: [option]
@spec start_link(options()) :: GenServer.on_start()
def start_link(options) do
instance_id = Keyword.fetch!(options, :instance_id)
GenServer.start_link(__MODULE__, State.new(options), name: via_tuple(instance_id))
end
@spec via_tuple(ConfigCat.instance_id()) :: {:via, module(), term()}
def via_tuple(instance_id) do
{:via, Registry, {ConfigCat.Registry, {__MODULE__, instance_id}}}
end
@impl GenServer
def init(%State{} = state) do
Logger.metadata(instance_id: state.instance_id)
{:ok, state}
end
@impl GenServer
def handle_call(:get_all_keys, _from, %State{} = state) do
result = do_get_all_keys(state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_value, key, default_value, user}, _from, %State{} = state) do
result = do_get_value(key, default_value, user, state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_value_details, key, default_value, user}, _from, %State{} = state) do
result = do_get_value_details(key, default_value, user, state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_all_value_details, user}, _from, %State{} = state) do
result =
state
|> do_get_all_keys()
|> Enum.map(&do_get_value_details(&1, nil, user, state))
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_key_and_value, variation_id}, _from, %State{} = state) do
case cached_config(state) do
{:ok, config, _fetch_time_ms} ->
result =
config
|> Config.settings()
|> Enum.find_value(nil, &entry_matching(&1, variation_id))
if is_nil(result) do
ConfigCatLogger.error(
"Could not find the setting for the specified variation ID: '#{variation_id}'",
event_id: 2011
)
end
{:reply, result, state}
_ ->
ConfigCatLogger.error(
"Config JSON is not present. Returning nil.",
event_id: 1000
)
{:reply, nil, state}
end
end
@impl GenServer
def handle_call({:get_all_values, user}, _from, %State{} = state) do
result =
state
|> do_get_all_keys()
|> Map.new(fn key -> {key, do_get_value(key, nil, user, state)} end)
{:reply, result, state}
end
@impl GenServer
def handle_call(:force_refresh, _from, %State{} = state) do
%{cache_policy: policy, instance_id: instance_id} = state
result = policy.force_refresh(instance_id)
{:reply, result, state}
end
@impl GenServer
def handle_call({:set_default_user, user}, _from, %State{} = state) do
{:reply, :ok, State.with_default_user(state, user)}
end
@impl GenServer
def handle_call(:clear_default_user, _from, %State{} = state) do
{:reply, :ok, State.clear_default_user(state)}
end
@impl GenServer
def handle_call(:set_online, _from, %State{} = state) do
%{cache_policy: policy, instance_id: instance_id} = state
result = policy.set_online(instance_id)
ConfigCatLogger.info("Switched to ONLINE mode.", event_id: 5200)
{:reply, result, state}
end
@impl GenServer
def handle_call(:set_offline, _from, %State{} = state) do
%{cache_policy: policy, instance_id: instance_id} = state
result = policy.set_offline(instance_id)
ConfigCatLogger.info("Switched to OFFLINE mode.", event_id: 5200)
{:reply, result, state}
end
@impl GenServer
def handle_call(:offline?, _from, %State{} = state) do
%{cache_policy: policy, instance_id: instance_id} = state
result = policy.offline?(instance_id)
{:reply, result, state}
end
defp do_get_value(key, default_value, user, %State{} = state) do
%EvaluationDetails{value: value} = evaluate(key, user, default_value, nil, state)
value
end
defp do_get_value_details(key, default_value, user, %State{} = state) do
evaluate(key, user, default_value, nil, state)
end
defp do_get_all_keys(%State{} = state) do
case cached_config(state) do
{:ok, config, _fetch_time_ms} ->
config |> Config.settings() |> Map.keys()
_ ->
ConfigCatLogger.error("Config JSON is not present. Returning empty result.",
event_id: 1000
)
[]
end
end
defp entry_matching({key, setting}, variation_id) do
case Setting.variation_value(setting, variation_id) do
nil -> nil
value -> {key, value}
end
end
defp evaluate(key, user, default_value, default_variation_id, %State{} = state) do
user = if user != nil, do: user, else: state.default_user
%EvaluationDetails{} =
details =
with {:ok, config, fetch_time_ms} <- cached_config(state),
{:ok, _settings} <- Config.fetch_settings(config),
{:ok, logger} <- EvaluationLogger.start() do
try do
%EvaluationDetails{} =
details =
Rollout.evaluate(key, user, default_value, default_variation_id, config, logger)
check_type_mismatch(details.value, default_value)
fetch_time =
case FetchTime.to_datetime(fetch_time_ms) do
{:ok, %DateTime{} = dt} -> dt
_ -> nil
end
%{details | fetch_time: fetch_time}
after
logger
|> EvaluationLogger.result()
|> ConfigCatLogger.debug(event_id: 5000)
EvaluationLogger.stop(logger)
end
else
_ ->
message =
"Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
ConfigCatLogger.error(message, event_id: 1000)
EvaluationDetails.new(
default_value?: true,
error: message,
key: key,
user: user,
value: default_value,
variation_id: default_variation_id
)
end
Hooks.invoke_on_flag_evaluated(state.instance_id, details)
details
end
defp cached_config(%State{} = state) do
%{cache_policy: policy, flag_overrides: flag_overrides, instance_id: instance_id} = state
local_config = OverrideDataSource.overrides(flag_overrides)
case OverrideDataSource.behaviour(flag_overrides) do
:local_only ->
{:ok, local_config, 0}
:local_over_remote ->
with {:ok, remote_config, fetch_time_ms} <- policy.get(instance_id) do
{:ok, Config.merge(remote_config, local_config), fetch_time_ms}
end
:remote_over_local ->
with {:ok, remote_config, fetch_time_ms} <- policy.get(instance_id) do
merged = Config.merge(local_config, remote_config)
{:ok, merged, fetch_time_ms}
end
end
end
defp check_type_mismatch(_value, nil), do: :ok
defp check_type_mismatch(value, default_value) do
value_type = SettingType.from_value(value)
default_type = SettingType.from_value(default_value)
number_types = [SettingType.double(), SettingType.int()]
cond do
value_type == default_type ->
:ok
value_type in number_types and default_type in number_types ->
:ok
true ->
ConfigCatLogger.warning(
"The type of a setting does not match the type of the specified default value (#{default_value}). " <>
"Setting's type was #{value_type} but the default value's type was #{default_type}. " <>
"Please make sure that using a default value not matching the setting's type was intended.",
event_id: 4002
)
end
end
end