Privacy-first web analytics for PhoenixKit — the numbers a hosted analytics product gives you, without the script tag.
What makes it different
Tracking happens server-side, in
PhoenixKitWebAnalytics.Plug. One line in the host's browser pipeline counts
every HTML response:
pipeline :browser do
# … existing plugs …
plug PhoenixKitWebAnalytics.Plug
endFrom there:
- Nothing is added to your pages. No script tag, no bundle to download, no render-blocking request, no third-party domain. Page weight and Core Web Vitals are exactly what they were.
- No cookies, no consent banner. Visitors are identified by a salted
hash of IP + User-Agent that rotates daily and is never stored in
reversible form — see
PhoenixKitWebAnalytics.Visitor. No IP address is written to the database. - Ad blockers can't remove it. There is no client-side request to block, so the numbers are the server's, not a script's.
- The data is yours. It lives in two tables in the host's own database.
For LiveView navigation (push_patch / push_navigate), add
PhoenixKitWebAnalytics.LiveHook to the live_session. For custom events
from the browser, there is an optional ~300-byte inline snippet —
PhoenixKitWebAnalytics.Web.Beacon — that is off by default.
Reports
Six admin pages under Web Analytics: an overview with the trend and
headline numbers, pages, acquisition (referrers / channels / campaigns),
technology (browsers, systems, devices, countries), custom events with a live
feed, and settings. Everything they show comes from
PhoenixKitWebAnalytics.Reports, which is a plain module you can call from
your own code:
import PhoenixKitWebAnalytics.Reports
"30d" |> then(&filter(period: &1)) |> top_paths(limit: 20)Installation
# host mix.exs
{:phoenix_kit_web_analytics, "~> 0.2"}Then mix deps.get and mix phoenix_kit.update (creates
phoenix_kit_web_analytics_events and
phoenix_kit_web_analytics_daily_stats), add the plug, and enable the module
on the admin Modules page.
Data growth
This is the one PhoenixKit table that grows with traffic rather than with
content. PhoenixKitWebAnalytics.Retention rolls completed days into daily
totals and prunes raw events past the retention window (365 days by default),
so the trend line is permanent while the row count is bounded.
Summary
Functions
Sidebar entries. Routes come from route_module/0, so no tab carries a
:live_view — these are navigation and active-state anchors only.
Background workers: the task supervisor that absorbs writes off the request path, and the hourly rollup/prune pass.
Turns tracking on, generating the visitor-hash salt if this is a first enable — so the very first request already hashes against a real secret.
Whether tracking is on.
Summary shown on the admin Modules page.
The events + daily stats tables (run by mix phoenix_kit.update).
Six admin pages plus the public collection endpoints.
Records a custom event from server-side code.
Records a page view from server-side code.
Functions
Sidebar entries. Routes come from route_module/0, so no tab carries a
:live_view — these are navigation and active-state anchors only.
Background workers: the task supervisor that absorbs writes off the request path, and the hourly rollup/prune pass.
Turns tracking on, generating the visitor-hash salt if this is a first enable — so the very first request already hashes against a real secret.
Whether tracking is on.
Defensive against the DB being unavailable (boot ordering, a test sandbox
owner that just stopped): every failure path answers false, so the plug
treats "we don't know" as "don't track".
Summary shown on the admin Modules page.
The events + daily stats tables (run by mix phoenix_kit.update).
Six admin pages plus the public collection endpoints.
Records a custom event from server-side code.
Use this for things that happen in your context modules rather than in the browser — an order placed, a subscription upgraded — where the event is a fact the server already knows and shouldn't depend on the client to report.
PhoenixKitWebAnalytics.track_event("order.placed", %{
path: "/checkout",
metadata: %{"total_cents" => 4900},
user_uuid: user.uuid
})attrs accepts any key from PhoenixKitWebAnalytics.Collector's hit shape.
Returns :ok immediately; the write happens in a supervised task.
Without :ip and :user_agent the event still records, but under a visitor
hash that won't match that person's page views — pass conn values through
when the event happens inside a request and you want it attributed to the
same visitor.
@spec track_pageview(map()) :: :ok
Records a page view from server-side code.
Only needed for pages the plug can't see — a response rendered by something other than the browser pipeline. Ordinary pages are already counted.