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

@@ -84,4 +84,104 @@ defmodule Berrypod.Analytics.BufferTest do
assert length(session_hashes) == 2
end
end
describe "supersede/1" do
test "removes a buffered event by id before flush" do
visitor_hash = :crypto.strong_rand_bytes(8)
ref = Ecto.UUID.generate()
Buffer.record(%{
id: ref,
name: "pageview",
pathname: "/",
visitor_hash: visitor_hash
})
Buffer.supersede(ref)
send(Buffer, :flush)
:timer.sleep(50)
events =
from(e in Event, where: e.visitor_hash == ^visitor_hash)
|> Repo.all()
assert events == []
end
test "only removes the event with the matching id" do
visitor_hash = :crypto.strong_rand_bytes(8)
ref = Ecto.UUID.generate()
Buffer.record(%{
id: ref,
name: "pageview",
pathname: "/",
visitor_hash: visitor_hash
})
Buffer.record(%{
name: "pageview",
pathname: "/about",
visitor_hash: visitor_hash
})
Buffer.supersede(ref)
send(Buffer, :flush)
:timer.sleep(50)
events =
from(e in Event, where: e.visitor_hash == ^visitor_hash)
|> Repo.all()
assert length(events) == 1
assert hd(events).pathname == "/about"
end
test "removes from DB if event was already flushed" do
visitor_hash = :crypto.strong_rand_bytes(8)
ref = Ecto.UUID.generate()
Buffer.record(%{
id: ref,
name: "pageview",
pathname: "/",
visitor_hash: visitor_hash
})
# Flush to DB first, simulating the 10s timer firing before supersede
send(Buffer, :flush)
:timer.sleep(50)
assert Repo.aggregate(from(e in Event, where: e.id == ^ref), :count) == 1
# Now supersede — should delete from DB
Buffer.supersede(ref)
:timer.sleep(50)
assert Repo.aggregate(from(e in Event, where: e.id == ^ref), :count) == 0
end
test "no-op when ref does not match any buffered event" do
visitor_hash = :crypto.strong_rand_bytes(8)
Buffer.record(%{
name: "pageview",
pathname: "/",
visitor_hash: visitor_hash
})
Buffer.supersede(Ecto.UUID.generate())
send(Buffer, :flush)
:timer.sleep(50)
events =
from(e in Event, where: e.visitor_hash == ^visitor_hash)
|> Repo.all()
assert length(events) == 1
end
end
end