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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user