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:
@@ -55,6 +55,20 @@ defmodule Berrypod.Analytics.Buffer do
|
||||
GenServer.cast(__MODULE__, {:record, attrs})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the Plug's initial pageview so the hook can replace it.
|
||||
|
||||
Looks in the ETS buffer first. If the event was already flushed to the DB
|
||||
(the 10s timer can fire between Plug record and hook connect), deletes it
|
||||
from there instead. Either way, the hook then records its own event with
|
||||
full data (screen_size etc.).
|
||||
|
||||
If JS never connects, the Plug's event stays and flushes normally.
|
||||
"""
|
||||
def supersede(ref) when is_binary(ref) do
|
||||
GenServer.cast(__MODULE__, {:supersede, ref})
|
||||
end
|
||||
|
||||
# ── GenServer callbacks ──
|
||||
|
||||
@impl true
|
||||
@@ -75,7 +89,7 @@ defmodule Berrypod.Analytics.Buffer do
|
||||
event =
|
||||
attrs
|
||||
|> Map.put(:session_hash, session_hash)
|
||||
|> Map.put(:id, Ecto.UUID.generate())
|
||||
|> Map.put_new(:id, Ecto.UUID.generate())
|
||||
|> Map.put(:inserted_at, DateTime.truncate(now, :second))
|
||||
|
||||
counter = state.counter + 1
|
||||
@@ -84,6 +98,29 @@ defmodule Berrypod.Analytics.Buffer do
|
||||
{:noreply, %{state | counter: counter, sessions: sessions}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:supersede, ref}, state) do
|
||||
found_in_ets =
|
||||
state.table
|
||||
|> :ets.tab2list()
|
||||
|> Enum.reduce(false, fn {key, event}, found ->
|
||||
if Map.get(event, :id) == ref do
|
||||
:ets.delete(state.table, key)
|
||||
true
|
||||
else
|
||||
found
|
||||
end
|
||||
end)
|
||||
|
||||
# If the 10s flush already moved it to the DB, delete it there
|
||||
unless found_in_ets do
|
||||
import Ecto.Query
|
||||
Repo.delete_all(from(e in Event, where: e.id == ^ref))
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:flush, state) do
|
||||
state = flush_events(state)
|
||||
|
||||
Reference in New Issue
Block a user