Who is on the site right now, and when they leave — from the LiveView processes themselves, with no client-side script.
A connected LiveView is a server process that lives exactly as long as the
visitor has the page open. PhoenixKitWebAnalytics.LiveHook registers each
one here; this server monitors it and keeps one row per open page in an ETS
table:
- Right now —
list/1andcount/1read the table directly, so the "on the site now" view is precise to the second rather than the usual "anyone with a hit in the last five minutes" approximation. - Leaving — when the process goes down (tab closed, navigated to
another LiveView or a non-LiveView page, network dropped) the monitor
fires and a
"leave"event is recorded with the time spent on the page. Apush_patchinside the same LiveView records a leave for the old URL too, vianavigate/2.
What "time on page" means here
Wall-clock time from the page being shown to the process ending. A page left
open in a background tab counts until the tab is closed or the socket is
dropped — browsers throttle and eventually disconnect idle background tabs,
which bounds it. max_engaged_ms/0 caps a single reading so one tab
forgotten over a weekend can't skew an average.
Reconnects are not exits
A LiveView process also ends when the connection drops for a moment (a
deploy, a flaky network) and the client rejoins with a new process. So a
leave is held for a short grace period (10 s by default,
config :phoenix_kit_web_analytics, presence_reconnect_grace_ms: ms); if the
same visitor rejoins the same page within it, the page continues — one
view, one leave, time counted from the original start.
The rejoin can also come first: on a reload Firefox opens the new page's
connection before closing the old one, which can linger for seconds. So a
page that opens while the same visitor already has the same page open takes
that view over, and the older one is hidden for up to 30 s
(presence_supersede_ms). If the older one goes down in that time it was a
reload — one view, one leave, as above. If it is still open after that, it
is a real second tab and is shown again.
Scope
The table is per node. On a multi-node deployment each node reports the
pages open on it; list/1 does not gather across the cluster.
Summary
Functions
Open pages counted per path, most open first — {path, count} pairs,
read from counters kept as pages open and close. limit caps the list.
Returns a specification to start this module under a supervisor.
How many pages are open right now.
Pages open right now, newest first. site restricts to one host.
The cap applied to one time-on-page reading, in milliseconds.
Records that a watched LiveView moved to a new URL without a remount
(push_patch / <.link patch>): a leave for the old path, and the clock
restarts for the new one.
One page of the open pages, newest first — for a site with thousands of
people online, the Right now list reads limit rows from an ordered index
instead of the whole table.
Whether the presence server is running on this node.
Stops watching a LiveView that patched to a page that isn't tracked (an excluded path): a leave for the page it was on, and it drops off the live list. Watching starts again if it patches back to a tracked page.
Starts watching a LiveView process that is showing path.
Types
Functions
@spec by_path(pos_integer()) :: [{String.t(), pos_integer()}]
Open pages counted per path, most open first — {path, count} pairs,
read from counters kept as pages open and close. limit caps the list.
One pass over the counters keeping only the top limit — no copy or sort
of every open path, which on a big site can be most of the open pages.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec count(String.t() | nil) :: non_neg_integer()
How many pages are open right now.
Pages open right now, newest first. site restricts to one host.
Reads every open page — fine for a test or a small site; the admin page
uses page/1 and by_path/1, which stay cheap at any size.
@spec max_engaged_ms() :: pos_integer()
The cap applied to one time-on-page reading, in milliseconds.
One page of the open pages, newest first — for a site with thousands of
people online, the Right now list reads limit rows from an ordered index
instead of the whole table.
Returns {visits, next_cursor}; pass next_cursor as :after for the
following page (nil when there is none).
@spec running?() :: boolean()
Whether the presence server is running on this node.
@spec unwatch(pid(), DateTime.t()) :: :ok
Stops watching a LiveView that patched to a page that isn't tracked (an excluded path): a leave for the page it was on, and it drops off the live list. Watching starts again if it patches back to a tracked page.
Starts watching a LiveView process that is showing path.
client carries the visitor's IP / User-Agent / language (used only to hash
the visitor for the leave event, exactly as for the page view — never
stored), and attrs the display fields for the live list. A no-op when the
server isn't running.