108 lines
3.1 KiB
Elixir
108 lines
3.1 KiB
Elixir
defmodule Aprsme.PlausibleAnalytics do
|
|
@moduledoc """
|
|
Server-side event tracking for Plausible Analytics (self-hosted at a.w5isp.com).
|
|
|
|
Fires an async HTTP POST to the Plausible Events API. All calls are fire-and-forget
|
|
— analytics must never block or error the caller.
|
|
|
|
## Configuration
|
|
|
|
config :aprsme, :plausible_analytics,
|
|
endpoint: "https://a.w5isp.com/api/event",
|
|
domain: "aprs.me"
|
|
|
|
## Usage
|
|
|
|
PlausibleAnalytics.track(conn, "ws_subscribe_bounds")
|
|
PlausibleAnalytics.track(conn, "api_callsign_lookup", %{callsign: "N0CALL"})
|
|
"""
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Track a custom event via the Plausible Events API.
|
|
|
|
`conn_or_metadata` accepts either a `Plug.Conn` (for HTTP requests) or a map with
|
|
`:remote_ip` / `:user_agent` / `:request_path` keys (for WebSocket channels).
|
|
"""
|
|
@spec track(Plug.Conn.t() | map(), String.t(), map()) :: :ok
|
|
def track(conn_or_metadata, event_name, props \\ %{})
|
|
|
|
def track(%Plug.Conn{} = conn, event_name, props) do
|
|
metadata = %{
|
|
user_agent: first_header(conn, "user-agent"),
|
|
remote_ip: remote_ip_string(conn),
|
|
request_path: conn.request_path
|
|
}
|
|
|
|
do_track(metadata, event_name, props)
|
|
end
|
|
|
|
def track(%{} = metadata, event_name, props) do
|
|
do_track(metadata, event_name, props)
|
|
end
|
|
|
|
defp do_track(metadata, event_name, props) do
|
|
_task =
|
|
Task.start(fn ->
|
|
send_event(metadata, event_name, props)
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
defp send_event(metadata, event_name, props) do
|
|
config = config()
|
|
|
|
url = "https://aprs.me#{Map.get(metadata, :request_path, "/")}"
|
|
|
|
body = maybe_add_props(%{name: event_name, domain: config[:domain], url: url}, props)
|
|
|
|
headers =
|
|
maybe_add_forwarded_for(
|
|
[{"content-type", "application/json"}, {"user-agent", Map.get(metadata, :user_agent, "aprs.me-server")}],
|
|
Map.get(metadata, :remote_ip) || Map.get(metadata, "remote_ip")
|
|
)
|
|
|
|
case Req.post(config[:endpoint], json: body, headers: headers, max_retries: 0, receive_timeout: 2_000) do
|
|
{:ok, %{status: status}} when status in 200..299 ->
|
|
Logger.debug("Plausible event tracked: #{event_name}")
|
|
|
|
{:ok, %{status: status}} ->
|
|
Logger.warning("Plausible event rejected (#{status}): #{event_name}")
|
|
|
|
{:error, error} ->
|
|
Logger.warning("Plausible event failed: #{event_name} — #{inspect(error)}")
|
|
end
|
|
end
|
|
|
|
defp maybe_add_props(body, props) when map_size(props) > 0 do
|
|
Map.put(body, :props, props)
|
|
end
|
|
|
|
defp maybe_add_props(body, _props), do: body
|
|
|
|
defp maybe_add_forwarded_for(headers, nil), do: headers
|
|
|
|
defp maybe_add_forwarded_for(headers, ip) do
|
|
[{"x-forwarded-for", ip} | headers]
|
|
end
|
|
|
|
defp first_header(conn, name) do
|
|
conn
|
|
|> Plug.Conn.get_req_header(name)
|
|
|> List.first()
|
|
end
|
|
|
|
# Extract remote_ip as a string, handling the tuple format from Plug.Conn
|
|
defp remote_ip_string(conn) do
|
|
case conn.remote_ip do
|
|
{a, b, c, d} -> "#{a}.#{b}.#{c}.#{d}"
|
|
other -> to_string(other)
|
|
end
|
|
end
|
|
|
|
defp config do
|
|
Application.get_env(:aprsme, :plausible_analytics, [])
|
|
end
|
|
end
|