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(
          client :: module,
          url :: String.t(),
          headers :: [{String.t(), binary}],
          opts :: keyword
        ) ::
          {:ok, status :: 200..599, headers :: [{String.t(), binary}], body :: term}
          | {:error, String.t()}
  def get(client, url, headers, opts) do
    {params, opts} = Keyword.pop(opts, :params, nil)
    retry(url, fn -> client.get(build_url(url, params), headers, opts) end, @max_retries)
  end

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

  @spec retry(String.t(), (-> {:ok, any, any, any} | {:error, String.t()}), non_neg_integer) ::
          {:ok, any, any, any} | {:error, String.t()}
  defp retry(url, fun, attempts_left) do
    case fun.() do
      {:ok, 200, _headers, "1"} = ok ->
        ok

      other ->
        attempts_left = attempts_left - 1

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

              "HTTP #{status}"

            {:error, reason} ->
              Logger.warning(%{
                message: "Won't retry to request due to a client error",
                attempts_left: attempts_left,
                url: url,
                error: reason
              })

              reason
          end

        if attempts_left > 0 do
          retry(url, fun, attempts_left)
        else
          {:error, reason}
        end
    end
  end

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