lib/mixpanel/client.ex

defmodule Mixpanel.Client do
  use GenServer

  @moduledoc """
  Mixpanel API Client GenServer.
  """

  require Logger
  alias Mixpanel.HTTP

  @type project_token :: String.t()
  @type active :: boolean
  @type base_url :: String.t()
  @type option :: {:project_token, project_token} | {:active, active} | {:base_url, base_url}
  @type init_args :: [option | GenServer.option(), ...]
  @type state :: %{
          required(:project_token) => project_token,
          required(:active) => active,
          required(:base_url) => base_url
        }

  @type event :: String.t() | map
  @type properties :: map
  @type alias_id :: String.t()
  @type distinct_id :: String.t()

  @base_url "https://api.mixpanel.com"
  @track_endpoint "/track"
  @engage_endpoint "/engage"
  @alias_endpoint "/track#identity-create-alias"

  @spec start_link(init_args) :: :ignore | {:error, any} | {:ok, pid}
  def start_link(init_args) do
    {opts, gen_server_opts} = Keyword.split(init_args, [:project_token, :active, :base_url])

    GenServer.start_link(__MODULE__, opts, gen_server_opts)
  end

  @spec child_spec(init_args) :: %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [init_args, ...]}
        }
  def child_spec(init_args) do
    init_args =
      init_args
      |> Keyword.put_new(:name, __MODULE__)
      |> Keyword.put_new(:base_url, @base_url)
      |> Keyword.put_new(:active, true)

    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [init_args]}
    }
  end

  @doc """
  Tracks a event

  See `Mixpanel.track/3`
  """
  @spec track(event, properties) :: :ok
  def track(event, properties) do
    GenServer.cast(__MODULE__, {:track, event, properties})
  end

  @doc """
  Updates a user profile.

  See `Mixpanel.engage/4`.
  """
  @spec engage(event | [event]) :: :ok
  def engage(event) do
    GenServer.cast(__MODULE__, {:engage, event})
  end

  @doc """
  Creates an alias for a user profile.

  See `Mixpanel.create_alias/2`.
  """
  @spec create_alias(alias_id, distinct_id) :: :ok
  def create_alias(alias, distinct_id) do
    GenServer.cast(__MODULE__, {:create_alias, alias, distinct_id})
  end

  @impl GenServer
  @spec init([option, ...]) :: {:ok, state}
  def init(opts) do
    project_token = Keyword.fetch!(opts, :project_token)
    active = Keyword.fetch!(opts, :active)
    base_url = Keyword.fetch!(opts, :base_url)

    {:ok,
     %{
       project_token: project_token,
       active: active,
       base_url: base_url
     }}
  end

  @spec handle_cast(
          {:track, event, properties}
          | {:engage, event}
          | {:create_alias, alias_id, distinct_id},
          state
        ) :: {:noreply, state}

  @impl GenServer
  def handle_cast(
        {:track, event, properties},
        %{project_token: project_token, active: true} = state
      ) do
    data =
      %{event: event, properties: Map.put(properties, :token, project_token)}
      |> Jason.encode!()
      |> :base64.encode()

    case HTTP.get(state.base_url <> @track_endpoint, [], params: [data: data]) do
      {:ok, _, _, _} ->
        :ok

      _ ->
        Logger.warning(%{message: "Problem tracking event", event: event, properties: properties})
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast({:engage, event}, %{project_token: project_token, active: true} = state) do
    data =
      event
      |> put_token(project_token)
      |> Jason.encode!()
      |> :base64.encode()

    case HTTP.get(state.base_url <> @engage_endpoint, [], params: [data: data]) do
      {:ok, _, _, _} ->
        :ok

      _ ->
        Logger.warning(%{message: "Problem tracking profile update", event: event})
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_cast(
        {:create_alias, alias, distinct_id},
        %{project_token: project_token, active: true} = state
      ) do
    data =
      %{
        event: "$create_alias",
        properties: %{
          token: project_token,
          alias: alias,
          distinct_id: distinct_id
        }
      }
      |> Jason.encode!()
      |> :base64.encode()

    case HTTP.post(
           state.base_url <> @alias_endpoint,
           "data=#{data}",
           [
             {"Content-Type", "application/x-www-form-urlencoded"}
           ]
         ) do
      {:ok, _, _, _} ->
        :ok

      :ignore ->
        Logger.warning(%{
          message: "Problem creating profile alias",
          alias: alias,
          distinct_id: distinct_id
        })
    end

    {:noreply, state}
  end

  # No events submitted when env configuration is set to false.
  def handle_cast(_request, %{active: false} = state) do
    {:noreply, state}
  end

  defp put_token(events, project_token) when is_list(events),
    do: Enum.map(events, &put_token(&1, project_token))

  defp put_token(event, project_token),
    do: Map.put(event, :"$token", project_token)
end