berrypod/lib/berrypod_web/analytics_hook.ex
jamey f91b47f0c3
All checks were successful
deploy / deploy (push) Successful in 1m37s
include browser/os/screen_size in e-commerce analytics events
Event call sites (product_view, add_to_cart, checkout_start, purchase)
were only passing visitor_hash and pathname, leaving browser, OS, screen
size and country nil. Add AnalyticsHook.attrs/1 helper to extract common
analytics fields from socket assigns, and use it in all LiveView event
call sites. Checkout controller reads the same fields from the session.

Also fix plug analytics test to clear stale events before assertions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 21:13:47 +00:00

115 lines
4.2 KiB
Elixir

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