berrypod/lib/berrypod_web/analytics_hook.ex

136 lines
4.9 KiB
Elixir
Raw Normal View History

defmodule BerrypodWeb.AnalyticsHook do
@moduledoc """
LiveView on_mount hook for analytics.
Reads session data prepared by the Plugs.Analytics plug (visitor hash,
browser, OS, referrer, UTMs) and records pageviews for SPA navigations.
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.
"""
import Phoenix.Component, only: [assign: 3]
import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1, get_connect_params: 1]
alias Berrypod.Analytics
alias Berrypod.Analytics.Buffer
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"])
|> assign(:analytics_plug_ref, session["analytics_plug_ref"])
socket =
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)
socket
|> assign(:analytics_screen_size, screen_size)
|> assign(:analytics_plug_ref, nil)
|> attach_hook(:analytics_events, :handle_event, &handle_analytics_event/3)
|> 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)
else
assign(socket, :analytics_screen_size, nil)
end
{:cont, socket}
end
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
defp handle_analytics_params(_params, uri, socket) do
if admin?(socket) do
{:cont, socket}
else
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)}
end
end
@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
defp handle_analytics_event("analytics:screen", %{"width" => width}, socket)
when is_integer(width) do
screen_size = classify_screen(width)
{:halt, assign(socket, :analytics_screen_size, screen_size)}
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