2026-02-22 12:50:55 +00:00
|
|
|
defmodule BerrypodWeb.AnalyticsHook do
|
|
|
|
|
@moduledoc """
|
2026-02-23 14:48:50 +00:00
|
|
|
LiveView on_mount hook for analytics.
|
2026-02-22 12:50:55 +00:00
|
|
|
|
2026-02-23 14:48:50 +00:00
|
|
|
Reads session data prepared by the Plugs.Analytics plug (visitor hash,
|
|
|
|
|
browser, OS, referrer, UTMs) and records pageviews for SPA navigations.
|
2026-02-22 12:50:55 +00:00
|
|
|
|
2026-02-23 14:48:50 +00:00
|
|
|
The Plug records an initial pageview into the ETS buffer with a unique
|
|
|
|
|
`plug_ref`. When JS connects, this hook tells the buffer to drop that
|
|
|
|
|
event (by ref) and records its own pageview with full data (screen_size).
|
|
|
|
|
If JS never connects (no-JS user), the Plug's event flushes to the DB
|
|
|
|
|
after the normal 10-second buffer interval. No timing heuristics needed.
|
|
|
|
|
|
|
|
|
|
Screen width is read from the LiveSocket connect params, so it's available
|
|
|
|
|
on every LiveView mount without relying on a separate JS hook event.
|
2026-02-22 12:50:55 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import Phoenix.Component, only: [assign: 3]
|
2026-02-23 14:48:50 +00:00
|
|
|
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, get_connect_params: 1]
|
2026-02-22 12:50:55 +00:00
|
|
|
|
|
|
|
|
alias Berrypod.Analytics
|
2026-02-23 14:48:50 +00:00
|
|
|
alias Berrypod.Analytics.Buffer
|
2026-02-22 12:50:55 +00:00
|
|
|
|
|
|
|
|
def on_mount(:track, _params, session, socket) do
|
|
|
|
|
socket =
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:analytics_visitor_hash, session["analytics_visitor_hash"])
|
|
|
|
|
|> assign(:analytics_browser, session["analytics_browser"])
|
|
|
|
|
|> assign(:analytics_os, session["analytics_os"])
|
|
|
|
|
|> assign(:analytics_referrer, session["analytics_referrer"])
|
|
|
|
|
|> assign(:analytics_referrer_source, session["analytics_referrer_source"])
|
|
|
|
|
|> assign(:analytics_utm_source, session["analytics_utm_source"])
|
|
|
|
|
|> assign(:analytics_utm_medium, session["analytics_utm_medium"])
|
|
|
|
|
|> assign(:analytics_utm_campaign, session["analytics_utm_campaign"])
|
|
|
|
|
|> assign(:analytics_country_code, session["country_code"])
|
2026-02-23 14:48:50 +00:00
|
|
|
|> assign(:analytics_plug_ref, session["analytics_plug_ref"])
|
2026-02-22 12:50:55 +00:00
|
|
|
|
|
|
|
|
socket =
|
2026-02-23 14:48:50 +00:00
|
|
|
if connected?(socket) do
|
|
|
|
|
# Supersede the Plug's buffered event — we'll record our own with screen_size
|
|
|
|
|
if plug_ref = socket.assigns[:analytics_plug_ref] do
|
|
|
|
|
Buffer.supersede(plug_ref)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
screen_size = screen_size_from_connect_params(socket)
|
|
|
|
|
|
2026-02-22 12:50:55 +00:00
|
|
|
socket
|
2026-02-23 14:48:50 +00:00
|
|
|
|> assign(:analytics_screen_size, screen_size)
|
|
|
|
|
|> assign(:analytics_plug_ref, nil)
|
2026-02-22 12:50:55 +00:00
|
|
|
|> attach_hook(:analytics_events, :handle_event, &handle_analytics_event/3)
|
2026-02-23 14:48:50 +00:00
|
|
|
|> then(fn s ->
|
|
|
|
|
if s.assigns.analytics_visitor_hash do
|
|
|
|
|
attach_hook(s, :analytics_params, :handle_params, &handle_analytics_params/3)
|
|
|
|
|
else
|
|
|
|
|
s
|
|
|
|
|
end
|
|
|
|
|
end)
|
2026-02-22 12:50:55 +00:00
|
|
|
else
|
2026-02-23 14:48:50 +00:00
|
|
|
assign(socket, :analytics_screen_size, nil)
|
2026-02-22 12:50:55 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
{:cont, socket}
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-23 14:48:50 +00:00
|
|
|
defp screen_size_from_connect_params(socket) do
|
|
|
|
|
case get_connect_params(socket) do
|
|
|
|
|
%{"screen_width" => width} when is_integer(width) -> classify_screen(width)
|
|
|
|
|
_ -> nil
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-22 12:50:55 +00:00
|
|
|
defp handle_analytics_params(_params, uri, socket) do
|
2026-02-23 14:48:50 +00:00
|
|
|
if admin?(socket) do
|
|
|
|
|
{:cont, socket}
|
2026-02-22 12:50:55 +00:00
|
|
|
else
|
2026-02-23 14:48:50 +00:00
|
|
|
pathname = URI.parse(uri).path
|
|
|
|
|
|
|
|
|
|
Analytics.track_pageview(%{
|
|
|
|
|
pathname: pathname,
|
|
|
|
|
visitor_hash: socket.assigns.analytics_visitor_hash,
|
|
|
|
|
referrer: socket.assigns.analytics_referrer,
|
|
|
|
|
referrer_source: socket.assigns.analytics_referrer_source,
|
|
|
|
|
utm_source: socket.assigns.analytics_utm_source,
|
|
|
|
|
utm_medium: socket.assigns.analytics_utm_medium,
|
|
|
|
|
utm_campaign: socket.assigns.analytics_utm_campaign,
|
|
|
|
|
country_code: socket.assigns.analytics_country_code,
|
|
|
|
|
screen_size: socket.assigns.analytics_screen_size,
|
|
|
|
|
browser: socket.assigns.analytics_browser,
|
|
|
|
|
os: socket.assigns.analytics_os
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Clear referrer/UTMs after first tracked navigation — they only apply to the entry
|
|
|
|
|
{:cont,
|
|
|
|
|
socket
|
|
|
|
|
|> assign(:analytics_referrer, nil)
|
|
|
|
|
|> assign(:analytics_referrer_source, nil)
|
|
|
|
|
|> assign(:analytics_utm_source, nil)
|
|
|
|
|
|> assign(:analytics_utm_medium, nil)
|
|
|
|
|
|> assign(:analytics_utm_campaign, nil)}
|
2026-02-22 12:50:55 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-22 21:13:47 +00:00
|
|
|
@doc """
|
|
|
|
|
Extracts common analytics attributes from socket assigns.
|
|
|
|
|
Call sites can merge their own fields (pathname, revenue, etc.) on top.
|
|
|
|
|
"""
|
|
|
|
|
def attrs(socket) do
|
|
|
|
|
%{
|
|
|
|
|
visitor_hash: socket.assigns[:analytics_visitor_hash],
|
|
|
|
|
browser: socket.assigns[:analytics_browser],
|
|
|
|
|
os: socket.assigns[:analytics_os],
|
|
|
|
|
screen_size: socket.assigns[:analytics_screen_size],
|
|
|
|
|
country_code: socket.assigns[:analytics_country_code]
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
2026-02-22 12:50:55 +00:00
|
|
|
defp handle_analytics_event("analytics:screen", %{"width" => width}, socket)
|
|
|
|
|
when is_integer(width) do
|
|
|
|
|
screen_size = classify_screen(width)
|
2026-02-23 14:48:50 +00:00
|
|
|
{:halt, assign(socket, :analytics_screen_size, screen_size)}
|
2026-02-22 12:50:55 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
defp handle_analytics_event(_event, _params, socket), do: {:cont, socket}
|
|
|
|
|
|
|
|
|
|
defp classify_screen(width) when width < 768, do: "mobile"
|
|
|
|
|
defp classify_screen(width) when width < 1024, do: "tablet"
|
|
|
|
|
defp classify_screen(_width), do: "desktop"
|
|
|
|
|
|
|
|
|
|
defp admin?(socket) do
|
|
|
|
|
case socket.assigns[:current_scope] do
|
|
|
|
|
%{user: %{}} -> true
|
|
|
|
|
_ -> false
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|