PhoenixKitWebAnalytics.Plug (PhoenixKitWebAnalytics v0.5.0)

Copy Markdown View Source

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
end

That'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-GET requests, 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: 1 or Sec-GPC: 1, when web_analytics_respect_dnt is on (it is by default)
  • automated User-Agents, unless web_analytics_track_bots is 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 the web_analytics_exclude_paths setting, 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

skip(conn)

@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.

skipped?(conn)

@spec skipped?(Plug.Conn.t()) :: boolean()

Whether this request has been marked to skip tracking.