Session recordings: how a visitor moved the pointer, what they clicked and hovered, how the page scrolled — enough to replay a visit over the page it happened on.
Off by default (web_analytics_recording). The optional client script
asks GET /phoenix-kit/analytics/recording once per page load whether to
record; while it does, it posts a chunk every few seconds (and one when
the page is hidden or left) to POST /phoenix-kit/analytics/recording.
What is never recorded
Text. No keystrokes, no form values, no page content, no element text —
only coordinates and short element selectors (main > form.signup > button:nth-of-type(2)). Visitors sending Do Not Track or Global Privacy
Control, bots, and excluded paths are never recorded.
Volume
A recorded page view is a chunk every ~10 seconds while something happens, so on a busy site recording everyone is a lot of rows. Two knobs:
web_analytics_recording_sample— the percent of visitors recorded (decided from the daily visitor hash, so a visitor is recorded for the whole day or not at all);web_analytics_recording_retention_days— recordings older than this are deleted by the retention pass (30 days by default).
Writes go through the collector's capped task pool, so under a flood chunks are dropped rather than queued.
Frame format (version 1)
A chunk's frames is %{"v" => 1, "f" => frames}; each frame is a list
starting with the milliseconds since the page was shown, then a type:
| Frame | Meaning |
|---|---|
[t, "m", x, y] | pointer at viewport x, y |
[t, "c", x, y, selector] | click (or tap) at x, y on selector |
[t, "h", selector] | pointer rested on selector (hover) |
[t, "s", scroll_x, scroll_y] | page scrolled to |
[t, "r", width, height] | viewport resized to |
[t, "v", 0 | 1] | page hidden / shown again |
The chunk carries the viewport size at the start of the page view; a
replay loads path at that size, applies the scrolls and draws the
pointer, clicks and hovers over it.
Summary
Functions
A visit's recording, page view by page view, oldest first:
[%{path, viewport_w, viewport_h, started_at, frames}], the frames of each
page's chunks joined in order. Stops at 100000 frames.
Deletes recordings older than the retention setting, in batches. Returns the number deleted.
Whether this request's visitor should be recorded on path: recording on,
the visitor sampled in, not a bot, not opted out, the path not excluded.
Whether a visit has a recording.
for_session/1 for the player, each page marked loadable when the player
may load it behind the recording: a path this visit really requested (a
page view the server recorded), not excluded from tracking. The path in a
chunk comes from the visitor's browser, so it is never loaded on its word
alone — an admin opening a replay must not be sent to a URL a visitor
chose.
Validates a chunk the client script posted and stores it, off the
caller's process. Returns :ok when it was accepted for writing and
{:error, reason} when it was refused — :not_recording, :invalid,
:too_fast (over 30 chunks a minute from one
visitor).
Functions
A visit's recording, page view by page view, oldest first:
[%{path, viewport_w, viewport_h, started_at, frames}], the frames of each
page's chunks joined in order. Stops at 100000 frames.
@spec prune() :: non_neg_integer()
Deletes recordings older than the retention setting, in batches. Returns the number deleted.
Whether this request's visitor should be recorded on path: recording on,
the visitor sampled in, not a bot, not opted out, the path not excluded.
Whether a visit has a recording.
for_session/1 for the player, each page marked loadable when the player
may load it behind the recording: a path this visit really requested (a
page view the server recorded), not excluded from tracking. The path in a
chunk comes from the visitor's browser, so it is never loaded on its word
alone — an admin opening a replay must not be sent to a URL a visitor
chose.
Validates a chunk the client script posted and stores it, off the
caller's process. Returns :ok when it was accepted for writing and
{:error, reason} when it was refused — :not_recording, :invalid,
:too_fast (over 30 chunks a minute from one
visitor).