Records one page view per HTML response — the whole tracker, server-side.
Add it once to the host's browser pipeline:
# lib/my_app_web/router.ex
pipeline :browser do
# … existing plugs …
plug PhoenixKitWebAnalytics.Plug
endThat's all page views need. No script tag, no client-side bundle, no
cookie, and nothing added to the rendered page — pages stay byte-for-byte
what they were. LiveView navigation, interactions and exits come from
PhoenixKitWebAnalytics.LiveHook, which has its own (small) setup.
Cost to a request
The plug does three cheap things in the request process: a method/path check,
one ETS read for settings, and register_before_send/2. When the response is
on its way out it builds a map and hands it to
PhoenixKitWebAnalytics.Collector, which does the settings-dependent
enrichment, the session-stitch query, and the insert in a supervised task.
No database work happens while the client is waiting.
What is skipped
Anything that isn't a person looking at a page:
- non-
GETrequests, and non-2xx responses (a redirect isn't a page view) - responses that aren't
text/html, so assets, JSON APIs, and file downloads never appear in reports - paths matching the exclusion patterns in settings (
/admin*by default) - requests sending
DNT: 1orSec-GPC: 1, whenweb_analytics_respect_dntis on (it is by default) - automated User-Agents, unless
web_analytics_track_botsis on - anything explicitly marked with
skip/1
Options
:exclude— extra path patterns on top of the ones in settings, e.g.plug PhoenixKitWebAnalytics.Plug, exclude: ["/healthz", "/internal*"]. A trailing*makes a pattern a prefix match. These apply to this plug's page views only: the LiveView hook can't see plug options, so a LiveView page to leave out entirely belongs in theweb_analytics_exclude_pathssetting, which both honour.
Client IP
The client address is visitor-hash input (and only that — no IP is ever
stored). It is read the way PhoenixKit reads it for a login
(PhoenixKit.Utils.IpAddress.client_address/1): a public
conn.remote_ip is the visitor; a private or loopback one is a reverse
proxy on the same box or network, and the visitor is the last
X-Forwarded-For entry — the one that proxy appended; a visitor can send
their own header, but not control what the proxy adds after it — then
X-Real-IP when there is no readable X-Forwarded-For. A proxy that
appends the port (203.0.113.7:51234, [2001:db8::7]:443) is read too.
That trust needs no configuration, so the proxy must set or append
X-Forwarded-For itself: one that only sets X-Real-IP passes the
visitor's own header through. On an intranet where visitors have private
addresses and reach the app directly, each of them can send it.
Behind a chain — a CDN in front of a load balancer — the last entry is the
CDN's address, not the visitor's. There, put a plug that rewrites
remote_ip from the headers your infrastructure controls —
remote_ip — before this one; a
public remote_ip is taken as is. The same goes for a CDN or proxy with a
public address in front of the app: as a public peer its headers are
ignored, and without RemoteIp every visitor is one of its addresses.
config :phoenix_kit_web_analytics, trust_x_forwarded_for: true is
deprecated and does nothing: the forwarded header from a private peer is
always read.
Summary
Functions
Marks a request as not-to-be-tracked.
Whether this request has been marked to skip tracking.
Functions
@spec skip(Plug.Conn.t()) :: Plug.Conn.t()
Marks a request as not-to-be-tracked.
Useful for endpoints that render HTML but aren't pages — a preview iframe, a health check that returns a status page, a LiveView upload target:
conn |> PhoenixKitWebAnalytics.Plug.skip() |> render("preview.html")Also honoured when set before this plug runs, e.g. from an earlier plug.
@spec skipped?(Plug.Conn.t()) :: boolean()
Whether this request has been marked to skip tracking.