Packages
unleash_fresha
1.14.1-git-e754
5.1.4
5.1.3
5.1.2
5.1.1
5.1.0
5.0.0
4.0.0
4.0.0-git-bda3
3.0.1
3.0.0
2.1.0
2.0.1
2.0.0-git-75d8
2.0.0-git-67b2
2.0.0-git-4b61
2.0.0-git-49f4
1.16.0
1.15.0
1.14.1-git-e754
1.14.0
1.14.0-git-71e9
1.13.0
1.13.0-git-4ba6
1.12.0
1.11.0
1.11.0-git-1b62
1.10.2
1.10.2-git-b835
1.10.1
1.10.0
1.9.3-alpha0
1.9.2
1.9.1
1.9.0
An Unleash Feature Flag client for Elixir, forked from [unleash](https://gitlab.com/afontaine/unleash_ex)
Current section
Files
Jump to
Current section
Files
lib/unleash/client.ex
defmodule Unleash.Client do
@moduledoc false
alias Unleash.Config
alias Unleash.Features
require Logger
@typedoc """
Type representing the result of attempting to fetch features from the Unleash API.
It can be:
* `{etag, features}` when fetched fresh features from the API and correspondent new HTTP etag.
* `{nil, reason}` in case of errors.
* `:cached` in case of HTTP-level caching
"""
@type features_result() ::
{String.t(), Features.t()}
| {nil, atom() | String.t()}
| :cached
@callback features(String.t()) :: features_result()
# WARNING: The return types of these typespecs are wrong
@callback register_client() :: Req.Response.t()
@callback metrics(map()) :: Req.Response.t()
@unleash_config Application.compile_env(:unleash_fresha, :unleash_req_options, [])
@appname "UNLEASH-APPNAME"
@instance_id "UNLEASH-INSTANCEID"
@if_none_match "If-None-Match"
@sdk_version "unleash_ex:#{Mix.Project.config()[:version]}"
@accept {"Accept", "application/json"}
@content_type {"Content-Type", "application/json"}
@telemetry_features_prefix [:unleash, :client, :fetch_features]
@telemetry_register_prefix [:unleash, :client, :register]
@telemetry_metrics_prefix [:unleash, :client, :push_metrics]
def features(etag \\ nil) do
headers = headers(etag)
url = "#{Config.url()}/client/features"
start_metadata = telemetry_metadata(%{etag: etag, url: url})
:telemetry.span(@telemetry_features_prefix, start_metadata, fn ->
[base_url: url, headers: headers]
|> Keyword.merge(@unleash_config)
|> Req.request(
# Tune Req retry behaviour for Unleash API polling
# We're polling periodically and some staleness is inherent in the Unleash architecture,
# therefore there's no critical need to retry every individual request.
# Additionally we seem to be incurring in https://github.com/sneako/finch/issues/272
# as the cause of most retry warnings. To work around those, one retry after a few ms
# should be more than enough.
max_retries: 1,
retry_delay: 50,
retry_log_level: false
)
|> case do
{:ok, response} ->
{result, metadata} = handle_feature_response(response)
{result, Map.merge(start_metadata, metadata)}
{:error, error} ->
Logger.error("Error refreshing features from Unleash API server: #{Exception.message(error)}")
{{nil, error.reason}, Map.put(start_metadata, :error, error.reason)}
end
end)
end
def register_client,
do:
register(%{
sdkVersion: @sdk_version,
strategies: Config.strategy_names(),
started: DateTime.to_iso8601(DateTime.utc_now()),
interval: Config.metrics_period()
})
def register(client) do
url = "#{Config.url()}/client/register"
start_metadata =
client
|> Map.take([:sdkVersion, :strategies, :interval])
|> Map.new(fn
{:sdkVersion, value} -> {:sdk_version, value}
{key, value} -> {key, value}
end)
|> Map.put(:url, url)
|> telemetry_metadata()
:telemetry.span(
@telemetry_register_prefix,
start_metadata,
fn ->
{result, metadata} = send_data(url, client)
{result, Map.merge(start_metadata, metadata)}
end
)
end
def metrics(met) do
url = "#{Config.url()}/client/metrics"
start_metadata = telemetry_metadata(%{url: url, metrics_payload: met})
:telemetry.span(@telemetry_metrics_prefix, start_metadata, fn ->
{result, metadata} = send_data(url, met)
{result, Map.merge(start_metadata, metadata)}
end)
end
defp handle_feature_response(response) do
case response do
%Req.Response{status: 304} ->
{:cached, %{http_response_status: 304}}
%Req.Response{status: 200} ->
pull_out_data(response)
%Req.Response{status: status} when is_integer(status) ->
{:cached, %{http_response_status: status}}
end
end
defp pull_out_data(response) do
features =
Features.from_map!(response.body)
etag =
response
|> Req.Response.get_header("etag")
|> Enum.at(0)
metadata = %{http_response_status: 200, etag: etag}
{{etag, features}, metadata}
end
defp send_data(url, data) do
[
url: url,
method: :post,
headers: headers(),
json: tag_data(data)
]
|> Keyword.merge(@unleash_config)
|> Req.request()
|> case do
{:ok, %Req.Response{status: status} = response} when is_integer(status) ->
{{:ok, response}, %{http_response_status: status}}
{:error, error} ->
{{:error, error.reason}, %{error: error.reason}}
end
end
defp headers(nil), do: headers()
defp headers(etag), do: headers() ++ [{@if_none_match, etag}]
defp headers,
do:
Config.custom_headers() ++
[{@appname, Config.appname()}, {@instance_id, Config.instance_id()}, @accept, @content_type]
defp tag_data(data) do
data
|> Map.put(:appName, Config.appname())
|> Map.put(:instanceId, Config.instance_id())
end
def telemetry_metadata(metadata \\ %{}) do
Map.merge(
%{appname: Config.appname(), instance_id: Config.instance_id()},
metadata
)
end
end