berrypod/lib/berrypod_web/plugs/analytics.ex

109 lines
3.2 KiB
Elixir
Raw Permalink Normal View History

defmodule BerrypodWeb.Plugs.Analytics do
@moduledoc """
Plug that records an initial pageview and prepares analytics session data.
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.
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.
"""
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
# 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
if admin?(conn) do
conn
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")
plug_ref = Ecto.UUID.generate()
Analytics.track_pageview(%{
id: plug_ref,
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,
screen_size: nil,
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)
|> put_session("analytics_plug_ref", plug_ref)
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