Page views from a LiveView, including the ones a page reload never happens for.
live_session :default, on_mount: [Pixelex.LiveView] do
live "/", HomeLive
live "/pricing", PricingLive
endWhat makes LiveView different
Three navigations that look the same to a user and nothing like each other to the server:
| in the template | server | client |
|---|---|---|
<.link href=> | full HTTP request, new mount | normal page load |
<.link navigate=> | old LiveView dismounted, new one mounted, no HTTP request | phx:navigate |
<.link patch=> | same LiveView, handle_params/3 only | phx:navigate |
Two of the three never reach a Plug, which is why a request-counting plug
alone reports a LiveView app as a one-page site. This hook attaches to
handle_params, which is the one callback all three run through.
Counting each visitor once — and why this hook needs Pixelex.Plug
mount/3 and handle_params/3 both run twice on a page load: once for
the dead render (plain HTML over HTTP) and again when the socket connects.
Count both and every human is two visitors.
The division of labour:
Pixelex.Plugrecords the dead render. It runs inside the HTTP request, so it has the real client IP, user agent andReferer. This is also what counts crawlers and clients whose socket never opens.- This hook records connected renders, and the initial one is dropped
by
Pixelex.Sessions.first_view?/3as a duplicate of what the plug just wrote — same visitor, same path, within seconds. live_patchandlive_navigatechange the path, so they pass the duplicate check and are counted. Neither ever reaches a plug.
The hook deliberately ignores dead renders, and this is not an
optimisation. get_connect_info/2 returns nothing during a dead render, so
there is no peer address to hash — the visitor would come out as a
different person from the one the plug recorded a moment earlier, and the
duplicate check would never fire. Every page load would count twice.
So plug Pixelex.Plug in your endpoint is required, not optional, for a
LiveView app. Without it, initial page views are not counted at all.
Reconnects
A dropped WiFi connection, a closed laptop lid, or a rolling deploy re-runs
mount/3 for every connected client. Left alone, a deploy inflates page
views by the size of the connected user base — a number that looks like
growth. LiveView sends _mounts in the connect params: 0 on a client's
first mount and higher on every rejoin after it, so a rejoin is skipped.
The referrer is only there once
Referer arrives on the initial HTTP request and never again — the
WebSocket has no such header. Pixelex.Plug stashes it in the session, and
this hook reads it back, so attribution survives the socket upgrade.
Options
on_mount: {Pixelex.LiveView, site_id: "shop", skip: &MyApp.admin?/1}
Summary
Functions
Is this mount a client rejoining, rather than arriving?
Record an event from inside a LiveView.
Functions
Is this mount a client rejoining, rather than arriving?
LiveView sends _mounts in the connect params: 0 the first time a client
mounts a page and higher on every rejoin after it — a dropped connection, a
closed laptop lid, a rolling deploy. Every one of those re-runs mount/3
for every connected client, so without this check a deploy inflates page
views by the size of the connected user base, which looks exactly like
growth.
Public because Phoenix.LiveViewTest overrides _mounts with its own
value and ignores put_connect_params/2 for that key, so this branch
cannot be reached through a simulated mount. Untestable code in the path of
every page view is not acceptable; a documented function is.
@spec track(Phoenix.LiveView.Socket.t(), String.t(), map()) :: Phoenix.LiveView.Socket.t()
Record an event from inside a LiveView.
Pixelex.LiveView.track(socket, "booking_completed", %{value: 1499.0})