defmodule ConfigCat do @moduledoc """ The ConfigCat Elixir SDK. `ConfigCat` provides a `Supervisor` that must be added to your applications supervision tree and an API for accessing your ConfigCat settings. ## Add ConfigCat to Your Supervision Tree Your application's supervision tree might need to be different, but the most basic approach is to add `ConfigCat` as a child of your top-most supervisor. ```elixir # lib/my_app/application.ex def start(_type, _args) do children = [ # ... other children ... {ConfigCat, [sdk_key: "YOUR SDK KEY"]} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end ``` ### Options `ConfigCat` takes a number of keyword arguments: - `sdk_key`: **REQUIRED** The SDK key for accessing your ConfigCat settings. Go to the [Connect your application](https://app.configcat.com/sdkkey) tab to get your SDK key. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY"]} ``` - `base_url`: **OPTIONAL** Allows you to specify a custom URL for fetching your ConfigCat settings. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", base_url: "https://my-cdn.example.com"]} ``` - `cache`: **OPTIONAL** Custom cache implementation. By default, `ConfigCat` uses its own in-memory cache, but you can also provide the name of a module that implements the `ConfigCat.ConfigCache` behaviour if you want to provide your own cache (e.g. based on Redis). If your cache implementation requires supervision, it is your application's responsibility to provide that. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", cache: MyCustomCacheModule]} ``` - `cache_policy`: **OPTIONAL** Specifies the [polling mode](https://configcat.com/docs/sdk-reference/elixir#polling-modes) used by `ConfigCat`. Defaults to auto-polling mode with a 60 second poll interval. You can specify a different polling mode or polling interval using `ConfigCat.CachePolicy.auto/1`, `ConfigCat.CachePolicy.lazy/1`, or `ConfigCat.CachePolicy.manual/0`. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", cache_policy: ConfigCat.CachePolicy.manual()]} ``` - `connect_timeout_milliseconds`: **OPTIONAL** timeout for establishing a TCP or SSL connection, in milliseconds. Default is 8000. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", connect_timeout_milliseconds: 8000]} ``` - `data_governance`: **OPTIONAL** Describes the location of your feature flag and setting data within the ConfigCat CDN. This parameter needs to be in sync with your Data Governance preferences. Defaults to `:global`. [More about Data Governance](https://configcat.com/docs/advanced/data-governance). ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", data_governance: :eu_only]} ``` - `default_user`: **OPTIONAL** user object that will be used as fallback when there's no user parameter is passed to the getValue() method. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", default_user: User.new("test@test.com")]} ``` - `flag_overrides`: **OPTIONAL** Specify a data source to use for [local flag overrides](https://configcat.com/docs/sdk-reference/elixir#flag-overrides). The data source must implement the `ConfigCat.OverrideDataSource` protocol. `ConfigCat.LocalFileDataSource` and `ConfigCat.LocalMapDataSource` are provided for you to use. - `hooks`: **OPTIONAL** Specify callback functions to be called when particular events are fired by the SDK. See `ConfigCat.Hooks`. - `http_proxy`: **OPTIONAL** Specify this option if you need to use a proxy server to access your ConfigCat settings. You can provide a simple URL, like `https://my_proxy.example.com` or include authentication information, like `https://user:password@my_proxy.example.com/`. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", http_proxy: "https://my_proxy.example.com"]} ``` - `name`: **OPTIONAL** A unique identifier for this instance of `ConfigCat`. Defaults to `ConfigCat`. Must be provided if you need to run more than one instance of `ConfigCat` in the same application. If you provide a `name`, you must then pass that name to all of the API functions using the `client` option. See `Multiple Instances` below. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", name: :unique_name]} ``` ```elixir ConfigCat.get_value("setting", "default", client: :unique_name) ``` - `offline`: **OPTIONAL** # Indicates whether the SDK should be initialized in offline mode or not. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", offline: true]} ``` - `read_timeout_milliseconds`: **OPTIONAL** timeout for receiving an HTTP response from the socket, in milliseconds. Default is 5000. ```elixir {ConfigCat, [sdk_key: "YOUR SDK KEY", read_timeout_milliseconds: 5000]} ``` ### Multiple Instances If you need to run more than one instance of `ConfigCat`, there are two ways you can do it. #### Module-Based You can create a module that `use`s `ConfigCat` and then call the ConfigCat API functions on that module. This is the recommended option, as it makes the calling code a bit clearer and simpler. You can pass any of the options listed above as arguments to `use ConfigCat` or specify them in your supervisor. Arguments specified by the supervisor take precedence over those provided to `use ConfigCat`. ```elixir # lib/my_app/first_flags.ex defmodule MyApp.FirstFlags do use ConfigCat, sdk_key: "sdk_key_1" end # lib/my_app/second_flags.ex defmodule MyApp.SecondFlags do use ConfigCat, sdk_key: "sdk_key_2" end # lib/my_app/application.ex def start(_type, _args) do children = [ # ... other children ... FirstFlags, SecondFlags, ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end # Calling code: FirstFlags.get_value("someKey", "default value") SecondFlags.get_value("otherKey", "other default") ``` #### Explicit Client If you prefer not to use the module-based solution, you can instead add multiple `ConfigCat` children to your application's supervision tree. You will need to give `ConfigCat` a unique `name` option for each, as well as using `Supervisor.child_spec/2` to provide a unique `id` for each instance. When calling the ConfigCat API functions, you'll pass a `client:` keyword argument with the unique `name` you gave to that instance. ```elixir # lib/my_app/application.ex def start(_type, _args) do children = [ # ... other children ... Supervisor.child_spec({ConfigCat, [sdk_key: "sdk_key_1", name: :first]}, id: :config_cat_1), Supervisor.child_spec({ConfigCat, [sdk_key: "sdk_key_2", name: :second]}, id: :config_cat_2), ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end # Calling code: ConfigCat.get_value("someKey", "default value", client: :first) ConfigCat.get_value("otherKey", "other default", client: :second) ``` ## Use the API Once `ConfigCat` has been started as part of your application's supervision tree, you can use its API to access your settings. ```elixir ConfigCat.get_value("isMyAwesomeFeatureEnabled", false) ``` By default, all of the public API functions will communicate with the default instance of the `ConfigCat` application. If you are running multiple instances of `ConfigCat`, you must provide the `client` option to the functions, passing along the unique name you specified above. ```elixir ConfigCat.get_value("isMyAwesomeFeatureEnabled", false, client: :second) ``` """ alias ConfigCat.CachePolicy alias ConfigCat.Client alias ConfigCat.Config alias ConfigCat.EvaluationDetails alias ConfigCat.Hooks alias ConfigCat.OverrideDataSource alias ConfigCat.User require ConfigCat.Constants, as: Constants @typedoc "Options that can be passed to all API functions." @type api_option :: {:client, instance_id()} @typedoc """ Data Governance mode [More about Data Governance](https://configcat.com/docs/advanced/data-governance) """ @type data_governance :: :eu_only | :global @typedoc "Identifier of a specific instance of `ConfigCat`." @type instance_id :: atom() @typedoc "The name of a configuration setting." @type key :: Config.key() @typedoc "An option that can be provided when starting `ConfigCat`." @type option :: {:base_url, String.t()} | {:cache, module()} | {:cache_policy, CachePolicy.t()} | {:connect_timeout_milliseconds, non_neg_integer()} | {:data_governance, data_governance()} | {:default_user, User.t()} | {:flag_overrides, OverrideDataSource.t()} | {:hooks, [Hooks.option()]} | {:http_proxy, String.t()} | {:name, instance_id()} | {:offline, boolean()} | {:read_timeout_milliseconds, non_neg_integer()} | {:sdk_key, String.t()} @type options :: [option()] @typedoc "The return value of the `force_refresh/1` function." @type refresh_result :: :ok | {:error, String.t()} @typedoc "The actual value of a configuration setting." @type value :: Config.value() @typedoc "The name of a variation being tested." @type variation_id :: Config.variation_id() @doc """ Builds a child specification to use in a Supervisor. Normally not called directly by your code. Instead, it will be called by your application's Supervisor once you add `ConfigCat` to its supervision tree. """ @spec child_spec(options()) :: Supervisor.child_spec() defdelegate child_spec(options), to: ConfigCat.Supervisor @doc """ Queries all settings keys in your configuration. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_all_keys([api_option()]) :: [key()] def get_all_keys(options \\ []) do options |> client() |> GenServer.call(:get_all_keys, Constants.fetch_timeout()) end @doc "See `get_value/4`." @spec get_value(key(), value(), User.t() | [api_option()]) :: value() def get_value(key, default_value, user_or_options \\ []) do if Keyword.keyword?(user_or_options) do get_value(key, default_value, nil, user_or_options) else get_value(key, default_value, user_or_options, []) end end @doc """ Retrieves a setting value from your configuration. Retrieves the setting named `key` from your configuration. To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a `ConfigCat.User` struct containing the information used by the targeting rules. Returns the value of the setting, or `default_value` if an error occurs. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_value(key(), value(), User.t() | nil, [api_option()]) :: value() def get_value(key, default_value, user, options) do options |> client() |> GenServer.call({:get_value, key, default_value, user}, Constants.fetch_timeout()) end @doc "See `get_value_details/4`." @spec get_value_details(key(), value(), User.t() | [api_option()]) :: EvaluationDetails.t() def get_value_details(key, default_value, user_or_options \\ []) do if Keyword.keyword?(user_or_options) do get_value_details(key, default_value, nil, user_or_options) else get_value_details(key, default_value, user_or_options, []) end end @doc """ Fetches the value and evaluation details of a feature flag or setting. Retrieves the setting named `key` from your configuration. To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a `ConfigCat.User` struct containing the information used by the targeting rules. Returns the evaluation details for the setting, including the value. If an error occurs while performing the evaluation, it will be captured in the `:error` field of the `ConfigCat.EvaluationDetails` struct. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_value_details(key(), value(), User.t() | nil, [api_option()]) :: EvaluationDetails.t() def get_value_details(key, default_value, user, options) do options |> client() |> GenServer.call({:get_value_details, key, default_value, user}, Constants.fetch_timeout()) end @doc "See `get_all_value_details/2`." @spec get_all_value_details(User.t() | [api_option()]) :: [EvaluationDetails.t()] def get_all_value_details(user_or_options \\ []) do if Keyword.keyword?(user_or_options) do get_all_value_details(nil, user_or_options) else get_all_value_details(user_or_options, []) end end @doc """ Fetches the values and evaluation details of all feature flags and settings. To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a `ConfigCat.User` struct containing the information used by the targeting rules. Returns evaluation details for all settings and feature flags, including their values. If an error occurs while performing the evaluation, it will be captured in the `:error` field of the individual `ConfigCat.EvaluationDetails` structs. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_all_value_details(User.t() | nil, [api_option()]) :: [EvaluationDetails.t()] def get_all_value_details(user, options) do options |> client() |> GenServer.call({:get_all_value_details, user}, Constants.fetch_timeout()) end @doc """ Fetches the name and value of the setting corresponding to a variation id. Returns a tuple containing the setting name and value, or `nil` if an error occurs. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_key_and_value(variation_id(), [api_option()]) :: {key(), value()} | nil def get_key_and_value(variation_id, options \\ []) do options |> client() |> GenServer.call({:get_key_and_value, variation_id}, Constants.fetch_timeout()) end @doc """ Fetches the values of all feature flags or settings from your configuration. To use ConfigCat's [targeting](https://configcat.com/docs/advanced/targeting) feature, provide a `ConfigCat.User` struct containing the information used by the targeting rules. Returns a map of all key value pairs. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec get_all_values(User.t() | nil, [api_option()]) :: %{key() => value()} def get_all_values(user, options \\ []) do options |> client() |> GenServer.call({:get_all_values, user}, Constants.fetch_timeout()) end @doc """ Force a refresh of the configuration from ConfigCat's CDN. Depending on the polling mode you're using, `ConfigCat` may automatically fetch your configuration during normal operation. Call this function to force a manual refresh when you want one. If you are using manual polling mode (`ConfigCat.CachePolicy.manual/0`), this is the only way to fetch your configuration. Returns `:ok`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec force_refresh([api_option()]) :: refresh_result() def force_refresh(options \\ []) do options |> client() |> GenServer.call(:force_refresh, Constants.fetch_timeout()) end @doc """ Sets the default user. Returns `:ok`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec set_default_user(User.t(), [api_option()]) :: :ok def set_default_user(user, options \\ []) do options |> client() |> GenServer.call({:set_default_user, user}, Constants.fetch_timeout()) end @doc """ Clears the default user. Returns `:ok`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec clear_default_user([api_option()]) :: :ok def clear_default_user(options \\ []) do options |> client() |> GenServer.call(:clear_default_user, Constants.fetch_timeout()) end @doc """ Configures the SDK to allow HTTP requests. Returns `:ok`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec set_online([api_option()]) :: :ok def set_online(options \\ []) do options |> client() |> GenServer.call(:set_online, Constants.fetch_timeout()) end @doc """ Configures the SDK to not initiate HTTP requests and work only from its cache. Returns `:ok`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec set_offline([api_option()]) :: :ok def set_offline(options \\ []) do options |> client() |> GenServer.call(:set_offline, Constants.fetch_timeout()) end @doc """ Returns `true` when the SDK is configured not to initiate HTTP requests, otherwise `false`. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec offline?([api_option()]) :: boolean() def offline?(options \\ []) do options |> client() |> GenServer.call(:offline?, Constants.fetch_timeout()) end @doc """ Return the current hook callbacks. ### Options - `client`: If you are running multiple instances of `ConfigCat`, provide the `client: :unique_name` option, specifying the name you configured for the instance you want to access. """ @spec hooks([api_option()]) :: Hooks.t() def hooks(options \\ []) do Keyword.get(options, :client, __MODULE__) end defp client(options) do options |> Keyword.get(:client, __MODULE__) |> Client.via_tuple() end defmacro __using__(default_options) do quote do @client Keyword.get(unquote(default_options), :name, __MODULE__) @spec child_spec(ConfigCat.options()) :: Supervisor.child_spec() def child_spec(options) do options = unquote(default_options) |> Keyword.merge(options) |> Keyword.put_new(:name, @client) Supervisor.child_spec({ConfigCat, options}, id: @client) end @spec get_all_keys :: [ConfigCat.key()] def get_all_keys do ConfigCat.get_all_keys(client: @client) end @spec get_value(ConfigCat.key(), ConfigCat.value(), ConfigCat.User.t() | nil) :: ConfigCat.value() def get_value(key, default_value, user \\ nil) do ConfigCat.get_value(key, default_value, user, client: @client) end @spec get_value_details(ConfigCat.key(), ConfigCat.value(), ConfigCat.User.t() | nil) :: ConfigCat.EvaluationDetails.t() def get_value_details(key, default_value, user \\ nil) do ConfigCat.get_value_details(key, default_value, user, client: @client) end @spec get_all_value_details(ConfigCat.User.t() | nil) :: [EvaluationDetails.t()] def get_all_value_details(user \\ nil) do ConfigCat.get_all_value_details(user, client: @client) end @spec get_key_and_value(ConfigCat.variation_id()) :: {ConfigCat.key(), ConfigCat.value()} | nil def get_key_and_value(variation_id) do ConfigCat.get_key_and_value(variation_id, client: @client) end @spec get_all_values(ConfigCat.User.t() | nil) :: %{ConfigCat.key() => ConfigCat.value()} def get_all_values(user \\ nil) do ConfigCat.get_all_values(user, client: @client) end @spec force_refresh :: ConfigCat.refresh_result() def force_refresh do ConfigCat.force_refresh(client: @client) end @spec set_default_user(ConfigCat.User.t()) :: :ok def set_default_user(user) do ConfigCat.set_default_user(user, client: @client) end @spec clear_default_user :: :ok def clear_default_user do ConfigCat.clear_default_user(client: @client) end @spec set_online :: :ok def set_online do ConfigCat.set_online(client: @client) end @spec set_offline :: :ok def set_offline do ConfigCat.set_offline(client: @client) end @spec offline? :: boolean() def offline? do ConfigCat.offline?(client: @client) end @spec hooks :: ConfigCat.Hooks.t() def hooks do ConfigCat.hooks(client: @client) end end end end