defmodule BerrypodWeb.AnalyticsHook do @moduledoc """ LiveView on_mount hook for analytics — Layer 2 of the progressive pipeline. Reads analytics data from the session (set by the Plugs.Analytics plug) and tracks subsequent SPA navigations via handle_params. Skips the initial mount since the plug already recorded that pageview. Also handles the `analytics:screen` event from the JS hook (Layer 3) to capture screen width for device classification. """ import Phoenix.Component, only: [assign: 3] import Phoenix.LiveView, only: [attach_hook: 4, connected?: 1] alias Berrypod.Analytics 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_screen_size, session["analytics_screen_size"]) |> assign(:analytics_country_code, session["country_code"]) |> assign(:analytics_initial_mount, true) socket = if connected?(socket) and socket.assigns.analytics_visitor_hash do socket |> attach_hook(:analytics_params, :handle_params, &handle_analytics_params/3) |> attach_hook(:analytics_events, :handle_event, &handle_analytics_event/3) else socket end {:cont, socket} end defp handle_analytics_params(_params, uri, socket) do # Skip the initial mount — the plug already recorded this pageview if socket.assigns.analytics_initial_mount do {:cont, assign(socket, :analytics_initial_mount, false)} else # Skip if admin user is browsing 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 SPA 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 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) {:cont, 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