README.md

# Unleash SDK (Fresha Edition)
An Elixir SDK for the [Unleash Feature Flag System](https://www.getunleash.io/).

Originally started as a fork of [unleash_ex](https://gitlab.com/afontaine/unleash_ex/),
but since then it has diverged quite a bit,
most notably by replacing [Mojito](https://github.com/appcues/mojito) with [Req](https://github.com/wojtekmach/req),
changing its usage APIs in version 2.0.0 and by implementing custom extensions like
[Propagation](https://hexdocs.pm/unleash_fresha/Unleash.Propagation.html).

Take a look at the [CHANGELOG](./CHANGELOG.md), the two projects diverged on version 1.9.0.

## Installation

Add `unleash_fresha` to your dependendencies in `mix.exs`:

```elixir
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.

```elixir
# 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`](https://github.com/dashbitco/mox).

As an example:

```elixir
# 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` and `get_variant/1,2,3` to the
  correspondent invokations of the macros `enabled?/1,2` and `get_variant/1,2`.  
  See the [CHANGELOG](./CHANGELOG.md) 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.

```elixir
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](https://hexdocs.pm/req/Req.html#module-header-names).

`:strategies` should be a module that implements
`c:Unleash.Strategies.strategies/0`. See [Extensibility](#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:

1. Create your new strategy. See `Unleash.Strategy` for details on the correct
    API and `Unleash.Strategy.Utils` for helpful utilities.

    ```elixir
    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
    ```

1. Create a new strategies module. See `Unleash.Strategies` for details on the correct
    API.

    ```elixir
    defmodule MyApp.Strategies do
      @behaviour Unleash.Strategies

      def strategies do
        [{"environment", MyApp.Strategy.Environment}] ++ Unleash.Strategies.strateges()
      end
    end
    ```

1. Configure your application to use your new strategies list.

    ```elixir
    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:

```elixir
# 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](https://github.com/Unleash/client-specification) which needs to be cloned to priv folder