2026-02-22 12:50:55 +00:00
|
|
|
defmodule BerrypodWeb.Plugs.Analytics do
|
|
|
|
|
@moduledoc """
|
2026-02-23 14:48:50 +00:00
|
|
|
Plug that records an initial pageview and prepares analytics session data.
|
2026-02-22 12:50:55 +00:00
|
|
|
|
2026-02-23 14:48:50 +00:00
|
|
|
Fires on every GET request regardless of JS — computes a privacy-friendly
|
|
|
|
|
visitor hash, parses the user agent, extracts referrer and UTM params.
|
|
|
|
|
Records the pageview immediately (for no-JS support) and stores the data
|
|
|
|
|
in the session for the LiveView hook to use on SPA navigations.
|
2026-02-22 12:50:55 +00:00
|
|
|
|
2026-02-23 14:48:50 +00:00
|
|
|
The event is recorded with a known ID (plug_ref) stored in the session.
|
|
|
|
|
When JS connects, the LiveView hook supersedes this event by ID and
|
|
|
|
|
records its own with full data (screen_size). If JS never connects,
|
|
|
|
|
the Plug's event flushes to the DB normally.
|
2026-02-22 12:50:55 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import Plug.Conn
|
|
|
|
|
|
|
|
|
|
alias Berrypod.Analytics
|
|
|
|
|
alias Berrypod.Analytics.{Salt, UAParser, Referrer}
|
|
|
|
|
|
|
|
|
|
def init(opts), do: opts
|
|
|
|
|
|
|
|
|
|
def call(%{method: "GET"} = conn, _opts) do
|
|
|
|
|
ua = get_req_header(conn, "user-agent") |> List.first("")
|
|
|
|
|
{browser, os} = UAParser.parse(ua)
|
|
|
|
|
|
|
|
|
|
# Skip bots
|
|
|
|
|
if browser == "Bot" do
|
|
|
|
|
conn
|
|
|
|
|
else
|
2026-02-23 14:48:50 +00:00
|
|
|
# Skip recording for logged-in admin — don't set analytics session
|
|
|
|
|
# data either, so downstream guards (visitor_hash checks in LiveViews)
|
|
|
|
|
# naturally filter out admin browsing for all event types
|
2026-02-22 12:50:55 +00:00
|
|
|
if admin?(conn) do
|
2026-02-23 14:48:50 +00:00
|
|
|
conn
|
2026-02-22 12:50:55 +00:00
|
|
|
else
|
|
|
|
|
record_and_prepare(conn, ua, browser, os)
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def call(conn, _opts), do: conn
|
|
|
|
|
|
|
|
|
|
defp record_and_prepare(conn, ua, browser, os) do
|
|
|
|
|
visitor_hash = Salt.hash_visitor(conn.remote_ip, ua)
|
|
|
|
|
{referrer, referrer_source} = extract_referrer(conn)
|
|
|
|
|
utm = extract_utms(conn)
|
|
|
|
|
country_code = get_session(conn, "country_code")
|
2026-02-23 14:48:50 +00:00
|
|
|
plug_ref = Ecto.UUID.generate()
|
2026-02-22 12:50:55 +00:00
|
|
|
|
|
|
|
|
Analytics.track_pageview(%{
|
2026-02-23 14:48:50 +00:00
|
|
|
id: plug_ref,
|
2026-02-22 12:50:55 +00:00
|
|
|
pathname: conn.request_path,
|
|
|
|
|
visitor_hash: visitor_hash,
|
|
|
|
|
referrer: referrer,
|
|
|
|
|
referrer_source: referrer_source,
|
|
|
|
|
utm_source: utm.source,
|
|
|
|
|
utm_medium: utm.medium,
|
|
|
|
|
utm_campaign: utm.campaign,
|
|
|
|
|
country_code: country_code,
|
2026-02-23 14:48:50 +00:00
|
|
|
screen_size: nil,
|
2026-02-22 12:50:55 +00:00
|
|
|
browser: browser,
|
|
|
|
|
os: os
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
conn
|
|
|
|
|
|> put_session("analytics_visitor_hash", visitor_hash)
|
|
|
|
|
|> put_session("analytics_browser", browser)
|
|
|
|
|
|> put_session("analytics_os", os)
|
|
|
|
|
|> put_session("analytics_referrer", referrer)
|
|
|
|
|
|> put_session("analytics_referrer_source", referrer_source)
|
|
|
|
|
|> put_session("analytics_utm_source", utm.source)
|
|
|
|
|
|> put_session("analytics_utm_medium", utm.medium)
|
|
|
|
|
|> put_session("analytics_utm_campaign", utm.campaign)
|
2026-02-23 14:48:50 +00:00
|
|
|
|> put_session("analytics_plug_ref", plug_ref)
|
2026-02-22 12:50:55 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp admin?(conn) do
|
|
|
|
|
case conn.assigns[:current_scope] do
|
|
|
|
|
%{user: %{}} -> true
|
|
|
|
|
_ -> false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp extract_referrer(conn) do
|
|
|
|
|
referrer_url = get_req_header(conn, "referer") |> List.first()
|
|
|
|
|
{referrer, source} = Referrer.parse(referrer_url)
|
|
|
|
|
|
|
|
|
|
# Don't count self-referrals
|
|
|
|
|
host = conn.host
|
|
|
|
|
|
|
|
|
|
if referrer && referrer == host do
|
|
|
|
|
{nil, nil}
|
|
|
|
|
else
|
|
|
|
|
{referrer, source}
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp extract_utms(conn) do
|
|
|
|
|
params = conn.query_params || %{}
|
|
|
|
|
|
|
|
|
|
%{
|
|
|
|
|
source: Map.get(params, "utm_source"),
|
|
|
|
|
medium: Map.get(params, "utm_medium"),
|
|
|
|
|
campaign: Map.get(params, "utm_campaign")
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
end
|