Packages
Official Split by Harness SDK for feature flags (a.k.a. Split FME)
Current section
Files
Jump to
Current section
Files
lib/split.ex
defmodule Split do
@moduledoc """
The Split.io Elixir thin client.
This module provides a simple API to interact with the Split.io service
via the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd).
## Adding Split to Your Supervision Tree
The most basic approach is to add `Split` as a child of your application's
top-most supervisor, i.e. `lib/my_app/application.ex`.
```elixir
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# ... other children ...
{Split, [socket_path: "/var/run/split.sock", fallback_enabled: true]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
You can also start `Split` dynamically by calling `Split.Supervisor.start_link/1`:
```elixir
Split.Supervisor.start_link(opts)
```
### Options
`Split` takes a number of keyword arguments as options when starting. The following options are available:
- `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`.
- `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`.
- `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html).
- `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`.
## Using the API
Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon to access feature flags and configurations.
```elixir
Split.get_treatment("user_key", "feature_name")
```
"""
alias Split.Telemetry
alias Split.Sockets.Pool
alias Split.Treatment
alias Split.RPC.Message
alias Split.RPC.ResponseParser
@type t :: %Split{
name: String.t(),
traffic_type: String.t(),
killed: boolean(),
treatments: [String.t()],
change_number: integer(),
configurations: map(),
default_treatment: String.t(),
flag_sets: [String.t()]
}
@typedoc "An option that can be provided when starting `Split`."
@type option ::
{:socket_path, String.t()}
| {:fallback_enabled, boolean()}
| {:pool_size, non_neg_integer()}
| {:connect_timeout, non_neg_integer()}
@type options :: [option()]
defstruct [
:name,
:traffic_type,
:killed,
:treatments,
:change_number,
:configurations,
:default_treatment,
:flag_sets
]
@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 `Split`
to its supervision tree.
"""
@spec child_spec(options()) :: Supervisor.child_spec()
defdelegate child_spec(options), to: Split.Supervisor
@spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) ::
{:ok, Treatment.t()} | {:error, term()}
def get_treatment(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do
request =
Message.get_treatment(
user_key: user_key,
feature_name: feature_name,
bucketing_key: bucketing_key,
attributes: attributes
)
execute_rpc(request)
end
@spec get_treatment_with_config(String.t(), String.t(), String.t() | nil, map() | nil) ::
{:ok, Treatment.t()} | {:error, term()}
def get_treatment_with_config(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do
request =
Message.get_treatment_with_config(
user_key: user_key,
feature_name: feature_name,
bucketing_key: bucketing_key,
attributes: attributes
)
execute_rpc(request)
end
@spec get_treatments(String.t(), [String.t()], String.t() | nil, map() | nil) ::
{:ok, %{String.t() => Treatment.t()}} | {:error, term()}
def get_treatments(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do
request =
Message.get_treatments(
user_key: user_key,
feature_names: feature_names,
bucketing_key: bucketing_key,
attributes: attributes
)
execute_rpc(request)
end
@spec get_treatments_with_config(String.t(), [String.t()], String.t() | nil, map() | nil) ::
{:ok, %{String.t() => Treatment.t()}} | {:error, term()}
def get_treatments_with_config(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do
request =
Message.get_treatments_with_config(
user_key: user_key,
feature_names: feature_names,
bucketing_key: bucketing_key,
attributes: attributes
)
execute_rpc(request)
end
@spec track(String.t(), String.t(), String.t(), term(), map()) :: :ok | {:error, term()}
def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do
request = Message.track(user_key, traffic_type, event_type, value, properties)
execute_rpc(request)
end
@spec split_names() :: {:ok, %{split_names: String.t()}} | {:error, term()}
def split_names do
request = Message.split_names()
execute_rpc(request)
end
@spec split(String.t()) :: {:ok, Split.t()} | {:error, term()}
def split(name) do
request = Message.split(name)
execute_rpc(request)
end
@spec splits() :: {:ok, [Split.t()]} | {:error, term()}
def splits do
request = Message.splits()
execute_rpc(request)
end
defp execute_rpc(request, opts \\ []) do
telemetry_span_context = :erlang.make_ref()
metadata = %{
rpc_call: Message.opcode_to_rpc_name(request.o),
telemetry_span_context: telemetry_span_context
}
Telemetry.span(:rpc, metadata, fn ->
request
|> Pool.send_message(opts)
|> ResponseParser.parse_response(request, span_context: telemetry_span_context)
|> case do
:ok ->
{:ok, %{}}
{:ok, data} = response ->
{response, %{response: data}}
{:error, reason} = error ->
{error, %{error: reason}}
end
end)
end
end