Per-tenant configuration, cached in ETS.
A site owns its domain, the event names a browser is allowed to send, its
ad-platform credentials, and its retention window. All of it lives in
pixelex_sites rather than in application config, because a library cannot
know a tenant's event names at compile time — that was the one thing wrong
with every version of this code that came before it.
Why the cache is not optional
POST /px/e runs the allowlist check on every single event. Reading a row
per event would put the busiest query in the system on the request path of
the cheapest operation in it. Sites change rarely, so a read-through ETS
cache with a TTL is the whole design; refresh/1 invalidates on write.
The allowlist
An unauthenticated endpoint that writes whatever string it is handed is a table anyone on the internet can fill, and the damage is not the disk — it is that every number on the dashboard becomes a claim that cannot be defended. So the server decides what exists. An unknown name is dropped silently and the endpoint answers 204 either way: telling a scanner which names are real is free help.
allow_any_event: true turns the allowlist off, for sites that only ever
emit events from trusted server-side code.
Summary
Functions
May this site record an event with this name from a browser?
A site declared in application config rather than the database.
Fetch a site, from cache when warm.
Create or update a site. Invalidates the cache.
Drop a site from the cache, so the next read reloads it.
Types
Functions
May this site record an event with this name from a browser?
A site declared in application config rather than the database.
config :pixelex,
sites: %{
"shop" => [
domain: "shop.test",
allowed_events: ~w(book_click call_click order_started)
]
}Most applications have exactly one site, and making them run a migration,
write an admin screen and insert a row before a single page view is counted
is a bad first five minutes. Config is checked first: it is explicit, it
costs no query, and it is version-controlled alongside the data-track
attributes whose names it lists.
The table is for the multi-tenant case, where sites are created by users at runtime and cannot be known when the release is built.
Fetch a site, from cache when warm.
Returns nil for an unknown id — the caller drops the event. An
auto-provisioning default would mean a typo in a tracking snippet silently
creates a tenant.
Create or update a site. Invalidates the cache.
@spec refresh(String.t()) :: :ok
Drop a site from the cache, so the next read reloads it.