lib/mixpanel/http.ex

defmodule Mixpanel.HTTP do
  @moduledoc """
  Adapter specification for HTTP clients and API for accessing them.
  """

  require Logger

  @max_retries 3

  @callback get(url :: String.t(), headers :: [{String.t(), binary}], opts :: keyword) ::
              {:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
              | {:error, String.t()}

  @callback post(
              url :: String.t(),
              body :: binary,
              headers :: [{String.t(), binary}],
              opts :: keyword
            ) ::
              {:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
              | {:error, String.t()}

  @spec get(url :: String.t(), headers :: [{String.t(), binary}], opts :: keyword) ::
          {:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
          | :ignore
  def get(url, headers \\ [], opts \\ []) do
    client = impl()
    {params, opts} = Keyword.pop(opts, :params, nil)
    retry(url, fn -> client.get(build_url(url, params), headers, opts) end, @max_retries)
  end

  @spec post(
          url :: String.t(),
          payload :: binary,
          headers :: [{String.t(), binary}],
          opts :: keyword
        ) ::
          {:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
          | :ignore
  def post(url, payload, headers \\ [], opts \\ []) do
    client = impl()
    retry(url, fn -> client.post(url, payload, headers, opts) end, @max_retries)
  end

  @spec impl() :: module
  def impl() do
    Application.get_env(:mixpanel_api_ex, :http_adapter, Mixpanel.HTTP.HTTPC)
  end

  @spec retry(String.t(), (-> {:ok, any, any, any} | {:error, String.t()}), pos_integer) ::
          {:ok, any, any, any} | :ignore
  defp retry(_url, _fun, 0) do
    :ignore
  end

  defp retry(url, fun, attempts_left) do
    case fun.() do
      {:ok, 200, _headers, "1"} = ok ->
        ok

      other ->
        attempts_left = attempts_left - 1

        case other do
          {:ok, status, _headers, _body} ->
            Logger.warning(%{
              message: "Retrying request",
              attempts_left: attempts_left,
              url: url,
              status: status
            })

          {:error, reason} ->
            Logger.warning(%{
              message: "Retrying request",
              attempts_left: attempts_left,
              url: url,
              error: reason
            })
        end

        retry(url, fun, attempts_left)
    end
  end

  defp build_url(url, nil), do: url
  defp build_url(url, data: data), do: "#{url}?#{URI.encode_query(data: data)}"
end