View Source Unleash SDK (Fresha Edition)
An Elixir SDK for the Unleash Feature Flag System.
Originally started as a fork of unleash_ex, but since then it has diverged quite a bit, most notably by replacing Mojito with Req, changing its usage APIs in version 2.0.0 and by implementing custom extensions like Propagation.
Take a look at the CHANGELOG, the two projects diverged on version 1.9.0.
Installation
Add unleash_fresha
to your dependendencies in mix.exs
:
def deps do
[
{:unleash_fresha, "~> 2.0"}
]
end
Usage
Use Unleash.Macros.enabled?/2
(or /1
) and Unleash.Macros.get_variant/2
(or /1
) to check if a feature
is enabled and get the right variant, in case of features with variants.
# You have to require the `Unleash.Macros` module.
# In this example we alias it to `Unleash` to make it nicer, but you do you.
iex> require Unleash.Macros, as: Unleash
iex> Unleash.enabled(:some_feature, fallback: false, context: %{user_id: "u-123"})
false
iex> Unleash.get_variant(:some_feature_with_variants, context: %{properties: %{"lucky" => "yes"}})
%{enabled: true, name: "variant_c", payload: %{}}
The macros do some static checks at compile-time for safer usage, and allow you to swap the runtime implementation that does the feature check, for example to use a mock one in the test environment (see next section).
If for some reason you can't use the macros, you can call the runtime equivalents in Unleash.Runtime
directly,
but by doing so you lose the benefits mentioned above.
Check the moduledocs of Unleash
, Unleash.Runtime
and Unleash.Macros
for more details.
Testing of code that uses feature flags
In tests, you might not want to connect to an Unleash Server, and/or you might want to force feature flag values to test specific code branches.
You can do so by swapping the SDK to use a different Runtime in tests,
e.g. a mock one via Mox
.
As an example:
# in config/test.exs
config :unleash_fresha, Unleash, runtime: Unleash.MockRuntime
# Somewhere in your test support files
Mox.defmock(Unleash.MockRuntime, for: Unleash)
# Your code can use Unleash as usual
defmodule SomeModule do
require Unleash.Macros, as: Unleash
def some_function() do
case Unleash.enabled?(:some_flag) do
true -> :yes
false -> :no
end
end
end
# In your tests, set expectations or stubs for feature flag checks
test "returns :yes when flag is enabled" do
# Note that the Runtime functions only have the 2-arity clauses,
# if you invoke the 1-arity macro, `opts` will be the default empty keyword list.
Mox.expect(Unleash.MockRuntime, :enabled?, 1, fn :some_flag, _opts -> true end)
assert :yes == SomeModule.some_function()
end
This is just one possible strategy. As another example, you could define a Runtime with a fixed behaviour instead of explicit per-test expectations.
Migrating from unleash_ex
- change the dependency name and version in mix.exs
- rename
:uneash
to:unleash_fresha
in config files - replace calls to functions
is_enabled/1,2,3
,enabled?/1,2,3
andget_variant/1,2,3
to the correspondent invokations of the macrosenabled?/1,2
andget_variant/1,2
.
See the CHANGELOG for version 2.0.0 and the docs of the macros for more details. - use any of the extra features provided by
unleash_fresha
, e.g. the gRPC interceptors.
Configuration
There are many configuration options available, and they are listed below with
their defaults. These go into the relevant config/*.exs
file.
config :unleash_fresha, Unleash,
url: "", # The URL of the Unleash server to connect to, should include up to http://base.url/api
appname: "", # The app name, used for registration
instance_id: "", # The instance ID, used for metrics tracking
metrics_period: 10 * 60 * 1000, # Send metrics every 10 minutes, in milliseconds
features_period: 15 * 1000, # Poll for new flags every 15 seconds, in milliseconds
strategies: Unleash.Strategies, # Which module to request for toggle strategies
backup_file: nil, # Backup file in the event that contacting the server fails
custom_http_headers: [], # A keyword list of custom headers to send to the server
runtime: Unleash.Runtime, # the Runtime module which will be used for feature flags checks
disable_client: false, # Whether or not to enable the client
disable_metrics: false, # Whether or not to send metrics,
retries: -1 # How many times to retry on failure, -1 disables limit
app_env: :dev # Which environment we're in
:custom_http_headers
should follow the format prescribed by Req.
:strategies
should be a module that implements
Unleash.Strategies.strategies/0
. See Extensibility
for more information.
Extensibility
If you need to create your own strategies, you can extend the Unleash
client
by implementing the callbacks in both Unleash.Strategy
and
Unleash.Strategies
as well as passing your new Strategies
module in as
configuration:
Create your new strategy. See
Unleash.Strategy
for details on the correct API andUnleash.Strategy.Utils
for helpful utilities.defmodule MyApp.Strategy.Environment do use Unleash.Strategy def enabled?(%{"environments" => environments}, _context) do with {:ok, environment} <- MyApp.Config.get_environment(), environment = List.to_string(environment) do {Utils.in_list?(environments, environment, &String.downcase/1), %{environment: environment, environments: environments}} end end end
Create a new strategies module. See
Unleash.Strategies
for details on the correct API.defmodule MyApp.Strategies do @behaviour Unleash.Strategies def strategies do [{"environment", MyApp.Strategy.Environment}] ++ Unleash.Strategies.strateges() end end
Configure your application to use your new strategies list.
config :unleash_fresha, Unleash, strategies: MyApp.Strategies
Telemetry events
From Unleash 1.9, telemetry events are emitted by the Unleash client
library. You can attach to these events and collect metrics or use the Logger
,
for example:
# An example of checking if Unleash server is reachable during the periodic
# features fetch.
:ok =
:telemetry.attach_many(
:duffel_core_feature_heatbeat_metric,
[
[:unleash, :client, :fetch_features, :stop],
[:unleash, :client, :fetch_features, :exception]
],
fn [:unleash, :client, :fetch_features, action],
_measurements,
metadata,
_config ->
require Logger
http_status = metadata[:http_response_status]
if action == :stop and http_status in [200, 304] do
Logger.info("Fetching features are ok")
else
Logger.info("Error on fetching features!!!")
end
end,
%{}
)
The following events are emitted by the Unleash library:
Event | When | Measurement | Metadata |
---|---|---|---|
[:unleash, :feature, :enabled?, :start] | dispatched by Unleash whenever a feature state has been requested. | %{system_time: system_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), feature: String.t()} |
| [:unleash, :feature, :enabled?, :stop]
| dispatched by Unleash whenever a feature check has successfully returned a result. | %{duration: native_time, monotonic_time: monotonic_time}
| %{appname: String.t(), instance_id: String.t(), feature: String.t(), result: boolean() | nil, reason: atom(), strategy_evaluations: [{String.t(), boolean()}], feature_enabled: boolean()}
|
| [:unleash, :feature, :enabled?, :exception]
| dispatched by Unleash after exceptions on fetching a feature's activation state. | %{duration: native_time, monotonic_time: monotonic_time}
| %{appname: String.t(), instance_id: String.t(), feature: String.t(), kind: :throw \| :error \| :exit, reason: term(), stacktrace: Exception.stacktrace()}
|
Event | When | Measurement | Metadata |
---|---|---|---|
[:unleash, :client, :fetch_features, :start] | dispatched by Unleash.Client whenever it start to fetch features from a remote Unleash server. | %{system_time: system_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), etag: String.t() | nil, url: String.t()} |
[:unleash, :client, :fetch_features, :stop] | dispatched by Unleash.Client whenever it finishes to fetch features from a remote Unleash server. | %{duration: native_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), etag: String.t() | nil, url: String.t(), http_response_status: pos_integer | nil, error: struct() | nil} |
[:unleash, :client, :fetch_features, :exception] | dispatched by Unleash.Client after exceptions on fetching features. | %{duration: native_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), etag: String.t() | nil, url: String.t(), kind: :throw | :error | :exit, reason: term(), stacktrace: Exception.stacktrace()} |
Event | When | Measurement | Metadata |
---|---|---|---|
[:unleash, :client, :register, :start] | dispatched by Unleash.Client whenever it starts to register in an Unleash server. | %{system_time: system_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), url: String.t(), sdk_version: String.t(), strategies: [String.t()], interval: pos_integer} |
[:unleash, :client, :register, :stop] | dispatched by Unleash.Client whenever it finishes to register in an Unleash server. | %{duration: native_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), url: String.t(), sdk_version: String.t(), strategies: [String.t()], interval: pos_integer, http_response_status: pos_integer | nil, error: struct() | nil} |
[:unleash, :client, :register, :exception] | dispatched by Unleash.Client after exceptions on registering in an Unleash server. | %{duration: native_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), url: String.t(), sdk_version: String.t(), strategies: [String.t()], interval: pos_integer, kind: :throw | :error | :exit, reason: term(), stacktrace: Exception.stacktrace()} |
Event | When | Measurement | Metadata |
---|---|---|---|
[:unleash, :client, :push_metrics, :start] | dispatched by Unleash.Client whenever it starts to push metrics to an Unleash server. | %{system_time: system_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), url: String.t(), metrics_payload: %{ :bucket => %{:start => String.t(), :stop => String.t(), toggles: %{ String.t() => %{ :yes => pos_integer(), :no => pos_integer() } } } } } |
[:unleash, :client, :push_metrics, :stop] | dispatched by Unleash.Client whenever it finishes to push metrics to an Unleash server. | %{duration: native_time, monotonic_time: monotonic_time} | %{appname: String.t(), instance_id: String.t(), url: String.t(), http_response_status: pos_integer | nil, error: struct() | nil, metrics_payload: %{ :bucket => %{:start => String.t(), :stop => String.t(), toggles: %{ String.t() => %{ :yes => pos_integer(), :no => pos_integer() } } } } } |
| [:unleash, :client, :push_metrics, :exception]
| dispatched by Unleash.Client after exceptions on pushing metrics to an Unleash server. | %{duration: native_time, monotonic_time: monotonic_time}
| %{appname: String.t(), instance_id: String.t(), url: String.t(), kind: :throw \| :error \| :exit, reason: term(), stacktrace: Exception.stacktrace(), metrics_payload: %{ :bucket => %{:start => String.t(), :stop => String.t(), toggles: %{ String.t() => %{ :yes => pos_integer(), :no => pos_integer() } } } } }
Event | When | Metadata |
---|---|---|
[:unleash, :repo, :schedule] | dispatched by Unleash.Repo when scheduling a poll to the server for metrics | %{appname: String.t(), instance_id: String.t(), retries: integer(), etag: String.t(), interval: pos_integer()} |
[:unleash, :repo, :backup_file_update] | dispatched by Unleash.Repo when it writes features to the backup file. | %{appname: String.t(), instance_id: String.t(), content: String.t(), filename: String.t()} |
[:unleash, :repo, :disable_polling] | dispatched by Unleash.Repo when polling gets disabled due to retries running out or zero retries being specified initially. | %{appname: String.t(), instance_id: String.t(), retries: integer(), etag: String.t()} |
[:unleash, :repo, :features_update] | dispatched by Unleash.Repo when features are updated. | %{appname: String.t(), instance_id: String.t(), retries: integer(), etag: String.t(), source: :remote | :cache | :backup_file} |
Testing
Tests are using upstream client-specification which needs to be cloned to priv folder