replace analytics double-count prevention with buffer supersede
All checks were successful
deploy / deploy (push) Successful in 1m13s
All checks were successful
deploy / deploy (push) Successful in 1m13s
The Plug records a pageview with a known ID (plug_ref) into the ETS buffer. When JS connects, the LiveView hook supersedes that event by ID and records its own with full data (screen_size from connect params). If JS never connects, the Plug's event flushes normally after 10s. Also fixes: admin browsing no longer leaks product_view events — the Plug now sets no analytics session data for admins, so all downstream visitor_hash guards naturally filter them out. Replaces the previous time-based skip logic which was brittle and race-prone. The supersede approach is deterministic and handles both the ETS buffer and already-flushed DB cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
defmodule BerrypodWeb.AnalyticsHook do
|
||||
@moduledoc """
|
||||
LiveView on_mount hook for analytics — Layer 2 of the progressive pipeline.
|
||||
LiveView on_mount hook for analytics.
|
||||
|
||||
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.
|
||||
Reads session data prepared by the Plugs.Analytics plug (visitor hash,
|
||||
browser, OS, referrer, UTMs) and records pageviews for SPA navigations.
|
||||
|
||||
Also handles the `analytics:screen` event from the JS hook (Layer 3) to
|
||||
capture screen width for device classification.
|
||||
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]
|
||||
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 =
|
||||
@@ -26,56 +32,71 @@ defmodule BerrypodWeb.AnalyticsHook do
|
||||
|> 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)
|
||||
|> assign(:analytics_plug_ref, session["analytics_plug_ref"])
|
||||
|
||||
socket =
|
||||
if connected?(socket) and socket.assigns.analytics_visitor_hash do
|
||||
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
|
||||
|> attach_hook(:analytics_params, :handle_params, &handle_analytics_params/3)
|
||||
|> 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
|
||||
socket
|
||||
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
|
||||
# Skip the initial mount — the plug already recorded this pageview
|
||||
if socket.assigns.analytics_initial_mount do
|
||||
{:cont, assign(socket, :analytics_initial_mount, false)}
|
||||
if admin?(socket) do
|
||||
{:cont, socket}
|
||||
else
|
||||
# Skip if admin user is browsing
|
||||
if admin?(socket) do
|
||||
{:cont, socket}
|
||||
else
|
||||
pathname = URI.parse(uri).path
|
||||
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
|
||||
})
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -96,7 +117,7 @@ defmodule BerrypodWeb.AnalyticsHook do
|
||||
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)}
|
||||
{:halt, assign(socket, :analytics_screen_size, screen_size)}
|
||||
end
|
||||
|
||||
defp handle_analytics_event(_event, _params, socket), do: {:cont, socket}
|
||||
|
||||
Reference in New Issue
Block a user