defmodule Plausible.Site.GateKeeper do @type policy() :: :allow | :not_found | :block | :throttle | :payment_required @policy_for_non_existing_sites :not_found @type t() :: {:allow, Plausible.Site.t()} | {:deny, policy()} @moduledoc """ Thin wrapper around `Plausible.RateLimit` for gate keeping domain-specific events during the ingestion phase. When the site is allowed, gate keeping check returns `:allow`, otherwise a `:deny` tagged tuple is returned with one of the following policy markers: * `:not_found` (indicates site not found in cache) * `:block` (indicates disabled sites) * `:throttle` (indicates rate limiting) Rate Limiting buckets are configured per site (externally via the CRM). See: `Plausible.Site` To look up each site's configuration, the RateLimiter fetches a Site by domain using `Plausible.Cache` interface. The module defines two policies outside the regular bucket inspection: * when the site is not found in cache: #{@policy_for_non_existing_sites} * when the underlying rate limiting mechanism returns an internal error: :allow """ alias Plausible.{Site, RateLimit} alias Plausible.Site.Cache @spec check(String.t(), Keyword.t()) :: t() def check(domain, opts \\ []) when is_binary(domain) do case policy(domain, opts) do {:allow, site} -> {:allow, site} other -> {:deny, other} end end @spec key(String.t()) :: String.t() def key(domain) do "ingest:site:#{domain}" end defp policy(domain, opts) when is_binary(domain) do with %Site{team: %{accept_traffic_until: accept_traffic_until}} = site <- Cache.get(domain, Keyword.get(opts, :cache_opts, [])), true <- Plausible.Sites.regular?(site) do if not is_nil(accept_traffic_until) and Date.after?(Date.utc_today(), accept_traffic_until) do :payment_required else check_rate_limit(site, opts) end else _ -> @policy_for_non_existing_sites end end defp check_rate_limit(%Site{ingest_rate_limit_threshold: nil} = site, _opts) do {:allow, site} end defp check_rate_limit(%Site{ingest_rate_limit_threshold: 0}, _opts) do :block end defp check_rate_limit(%Site{ingest_rate_limit_threshold: threshold} = site, opts) when is_integer(threshold) do key = Keyword.get(opts, :key, key(site.domain)) scale_ms = site.ingest_rate_limit_scale_seconds * 1_000 case RateLimit.check_rate(key, scale_ms, threshold) do {:deny, _} -> :throttle {:allow, _} -> {:allow, site} end end end