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