defmodule Kameleoon.Client do
@moduledoc """
Elixir wrapper over a native Kameleoon client Rustler resource.
"""
alias Kameleoon.Error
alias Kameleoon.CookieAccessor
alias Kameleoon.Native.Events
alias Kameleoon.Native.Nif
alias Kameleoon.Types.DataFile
alias Kameleoon.Types.RemoteVisitorDataFilter
alias Kameleoon.Types.Variation
alias Kameleoon.Internal.GetVisitorCodeResult
alias Kameleoon.Internal.NativeResult
defstruct [:native, :site_code, :environment, events: Events]
@type t :: %__MODULE__{
native: term(),
site_code: String.t() | nil,
environment: String.t() | nil,
events: GenServer.server()
}
@native_callback_timeout 30_000
@native_reply_tag :kameleoon_native
@spec initialize(t(), keyword()) :: :ok | {:error, Error.t()}
def initialize(client, opts \\ []) do
do_initialize(client, opts)
end
@spec is_ready?(t()) :: boolean()
def is_ready?(client) do
Nif.is_ready(client.native)
end
@spec get_visitor_code(t(), CookieAccessor.t(), String.t() | nil) ::
{:ok, String.t(), CookieAccessor.state()} | {:error, Error.t()}
def get_visitor_code(client, cookies, default_visitor_code \\ nil)
def get_visitor_code(client, cookies, default_visitor_code) do
request_cookies = CookieAccessor.request_cookies(cookies)
case native_get_visitor_code(client, request_cookies, default_visitor_code) do
{:ok, %GetVisitorCodeResult{visitor_code: visitor_code, cookies: cookie_payload}} ->
{:ok, visitor_code, CookieAccessor.apply_response_cookies(cookies, cookie_payload)}
error ->
error
end
end
@spec set_legal_consent(t(), String.t(), boolean(), CookieAccessor.t() | nil) ::
{:ok, CookieAccessor.state() | nil} | {:error, Error.t()}
def set_legal_consent(client, visitor_code, consent, cookies \\ nil) do
request_cookies = if cookies, do: CookieAccessor.request_cookies(cookies)
case native_set_legal_consent(client, visitor_code, consent, request_cookies) do
{:ok, cookie_payload} ->
{:ok, cookies && CookieAccessor.apply_response_cookies(cookies, cookie_payload)}
error ->
error
end
end
@spec add_data(t(), String.t(), struct() | [struct()], keyword()) :: :ok | {:error, Error.t()}
def add_data(client, visitor_code, data, opts \\ []) do
NativeResult.ok(Nif.add_data(client.native, visitor_code, List.wrap(data), Keyword.get(opts, :track, true)))
end
@spec flush(t(), String.t()) :: :ok | {:error, Error.t()}
def flush(client, visitor_code) do
NativeResult.ok(Nif.flush(client.native, visitor_code))
end
@spec flush_instant(t(), String.t()) :: :ok | {:error, Error.t()}
def flush_instant(client, visitor_code) do
with {:ok, ref} <- start_flush_instant(client, visitor_code) do
await_native_ok(:ok, ref)
end
end
@spec track_conversion(t(), String.t(), non_neg_integer(), keyword()) ::
:ok | {:error, Error.t()}
def track_conversion(client, visitor_code, goal_id, opts \\ []) do
NativeResult.ok(
Nif.track_conversion(
client.native,
visitor_code,
goal_id,
Keyword.get(opts, :revenue),
Keyword.get(opts, :negative, false),
Keyword.get(opts, :metadata)
)
)
end
@spec is_feature_active?(t(), String.t(), String.t(), keyword()) ::
{:ok, boolean()} | {:error, Error.t()}
def is_feature_active?(client, visitor_code, feature_key, opts \\ []) do
NativeResult.result(
Nif.is_feature_active(
client.native,
visitor_code,
feature_key,
Keyword.get(opts, :track, true)
)
)
end
@spec get_variation(t(), String.t(), String.t(), keyword()) ::
{:ok, Variation.t()} | {:error, Error.t()}
def get_variation(client, visitor_code, feature_key, opts \\ []) do
NativeResult.result(
Nif.get_variation(
client.native,
visitor_code,
feature_key,
Keyword.get(opts, :track, true)
)
)
end
@spec get_variations(t(), String.t(), keyword()) ::
{:ok, %{String.t() => Variation.t()}} | {:error, Error.t()}
def get_variations(client, visitor_code, opts \\ []) do
NativeResult.result(
Nif.get_variations(
client.native,
visitor_code,
Keyword.get(opts, :only_active, false),
Keyword.get(opts, :track, true)
)
)
end
@spec set_forced_variation(t(), String.t(), non_neg_integer(), String.t() | nil, keyword()) ::
:ok | {:error, Error.t()}
def set_forced_variation(client, visitor_code, experiment_id, variation_key \\ nil, opts \\ []) do
NativeResult.ok(
Nif.set_forced_variation(
client.native,
visitor_code,
experiment_id,
variation_key,
Keyword.get(opts, :force_targeting, true)
)
)
end
@spec evaluate_audiences(t(), String.t()) :: :ok | {:error, Error.t()}
def evaluate_audiences(client, visitor_code) do
NativeResult.ok(Nif.evaluate_audiences(client.native, visitor_code))
end
@spec get_engine_tracking_code(t(), String.t()) ::
{:ok, String.t()} | {:error, Error.t()}
def get_engine_tracking_code(client, visitor_code) do
NativeResult.result(Nif.get_engine_tracking_code(client.native, visitor_code))
end
@spec get_remote_data(t(), String.t()) :: {:ok, String.t()} | {:error, Error.t()}
def get_remote_data(client, key) do
with {:ok, ref} <- start_get_remote_data(client, key) do
await(ref)
end
end
@spec get_remote_visitor_data(t(), String.t(), keyword()) :: :ok | {:error, Error.t()}
def get_remote_visitor_data(client, visitor_code, opts \\ []) do
with {:ok, ref} <- start_get_remote_visitor_data(client, visitor_code, remote_visitor_data_filter(opts)) do
await_native_ok(:ok, ref)
end
end
@spec get_visitor_warehouse_audience(t(), String.t(), non_neg_integer(), keyword()) ::
:ok | {:error, Error.t()}
def get_visitor_warehouse_audience(client, visitor_code, custom_data_index, opts \\ []) do
with {:ok, ref} <-
start_get_visitor_warehouse_audience(
client,
visitor_code,
custom_data_index,
Keyword.get(opts, :warehouse_key)
) do
await_native_ok(:ok, ref)
end
end
@spec get_datafile(t()) :: {:ok, DataFile.t()} | {:error, Error.t()}
def get_datafile(client) do
NativeResult.result(Nif.get_datafile(client.native))
end
@spec on_datafile_update(t(), (-> any()) | nil) :: :ok | {:error, Error.t()}
def on_datafile_update(client, fun) when is_function(fun, 0) or is_nil(fun) do
Events.set_handler(client, fun, client.events)
end
defp native_get_visitor_code(client, request_cookies, default_visitor_code) do
NativeResult.result(Nif.get_visitor_code(client.native, request_cookies, default_visitor_code))
end
defp native_set_legal_consent(client, visitor_code, consent, request_cookies) do
NativeResult.result(Nif.set_legal_consent(client.native, visitor_code, consent, request_cookies))
end
defp do_initialize(client, opts) do
with {:ok, ref} <- start_initialize(client, opts) do
await_native_ok(:ok, ref)
end
end
defp start_initialize(client, opts) do
ref = make_ref()
client.native
|> Nif.initialize(Keyword.get(opts, :timeout), self(), ref)
|> NativeResult.ref(ref)
end
defp start_flush_instant(client, visitor_code) do
ref = make_ref()
client.native
|> Nif.flush_instant(visitor_code, self(), ref)
|> NativeResult.ref(ref)
end
defp start_get_remote_data(client, key) do
ref = make_ref()
client.native
|> Nif.get_remote_data(key, self(), ref)
|> NativeResult.ref(ref)
end
defp start_get_remote_visitor_data(client, visitor_code, filter) do
ref = make_ref()
client.native
|> Nif.get_remote_visitor_data(visitor_code, filter, self(), ref)
|> NativeResult.ref(ref)
end
defp remote_visitor_data_filter(opts) do
case Keyword.get(opts, :filter) do
%RemoteVisitorDataFilter{} = filter -> filter
nil -> nil
end
end
defp start_get_visitor_warehouse_audience(
client,
visitor_code,
custom_data_index,
warehouse_key
) do
ref = make_ref()
client.native
|> Nif.get_visitor_warehouse_audience(
visitor_code,
custom_data_index,
warehouse_key,
self(),
ref
)
|> NativeResult.ref(ref)
end
defp await(ref) when is_reference(ref) do
await_native_result(:ok, ref)
end
defp await_native_ok(status, ref, timeout \\ @native_callback_timeout)
defp await_native_ok(:ok, ref, timeout) do
case await_native_result(:ok, ref, timeout) do
{:ok, _result} -> :ok
error -> error
end
end
defp await_native_result(status, ref, timeout \\ @native_callback_timeout)
defp await_native_result(:ok, ref, timeout) do
receive do
{@native_reply_tag, ^ref, {:ok, result}} -> {:ok, result}
{@native_reply_tag, ^ref, {:error, payload}} -> NativeResult.error(payload)
after
timeout -> {:error, %Error{code: "Internal", message: "Native operation timed out"}}
end
end
end