replace analytics double-count prevention with buffer supersede
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:
jamey
2026-02-23 14:48:50 +00:00
parent 7ceee9c814
commit 162a5bfe9a
6 changed files with 231 additions and 68 deletions

View File

@@ -1,14 +1,16 @@
defmodule BerrypodWeb.Plugs.Analytics do
@moduledoc """
Plug that records a pageview event on every shop HTTP request.
Plug that records an initial pageview and prepares analytics session data.
This is Layer 1 of the progressive analytics pipeline — it fires on every
GET request regardless of whether JS is enabled. Computes a privacy-friendly
visitor hash, parses the user agent, extracts referrer and UTM params, and
buffers the event for batch writing to SQLite.
Fires on every GET request regardless of JS — computes a privacy-friendly
visitor hash, parses the user agent, extracts referrer and UTM params.
Records the pageview immediately (for no-JS support) and stores the data
in the session for the LiveView hook to use on SPA navigations.
Also stores analytics data in the session so the LiveView hook (Layer 2)
can read it for subsequent SPA navigations.
The event is recorded with a known ID (plug_ref) stored in the session.
When JS connects, the LiveView hook supersedes this event by ID and
records its own with full data (screen_size). If JS never connects,
the Plug's event flushes to the DB normally.
"""
import Plug.Conn
@@ -26,9 +28,11 @@ defmodule BerrypodWeb.Plugs.Analytics do
if browser == "Bot" do
conn
else
# Skip if the logged-in admin is browsing
# Skip recording for logged-in admin — don't set analytics session
# data either, so downstream guards (visitor_hash checks in LiveViews)
# naturally filter out admin browsing for all event types
if admin?(conn) do
prepare_session(conn, ua, browser, os)
conn
else
record_and_prepare(conn, ua, browser, os)
end
@@ -42,9 +46,10 @@ defmodule BerrypodWeb.Plugs.Analytics do
{referrer, referrer_source} = extract_referrer(conn)
utm = extract_utms(conn)
country_code = get_session(conn, "country_code")
screen_size = get_session(conn, "analytics_screen_size")
plug_ref = Ecto.UUID.generate()
Analytics.track_pageview(%{
id: plug_ref,
pathname: conn.request_path,
visitor_hash: visitor_hash,
referrer: referrer,
@@ -53,7 +58,7 @@ defmodule BerrypodWeb.Plugs.Analytics do
utm_medium: utm.medium,
utm_campaign: utm.campaign,
country_code: country_code,
screen_size: screen_size,
screen_size: nil,
browser: browser,
os: os
})
@@ -67,16 +72,7 @@ defmodule BerrypodWeb.Plugs.Analytics do
|> put_session("analytics_utm_source", utm.source)
|> put_session("analytics_utm_medium", utm.medium)
|> put_session("analytics_utm_campaign", utm.campaign)
end
# Store session data for the hook even when we skip recording for admins
defp prepare_session(conn, ua, browser, os) do
visitor_hash = Salt.hash_visitor(conn.remote_ip, ua)
conn
|> put_session("analytics_visitor_hash", visitor_hash)
|> put_session("analytics_browser", browser)
|> put_session("analytics_os", os)
|> put_session("analytics_plug_ref", plug_ref)
end
defp admin?(conn) do