Skip to main content

Session Recordings

Session recordings let you replay exactly what a user did on your site — mouse movements, clicks, scrolling, and form interactions — without capturing any sensitive input data.

How Recordings Work

Recording uses rrweb, an open-source library that serialises the DOM and records mutations, pointer events, and scroll events into a structured event stream.

The recording data never leaves the browser in one large payload. Instead, it is split into segments (chunks of ~60 seconds each) sent incrementally to the Platform. This keeps individual requests small and allows partial recovery if the user leaves mid-session.


Session Segments

A recording session is composed of numbered segments: 0, 1, 2, 3, ...

Watching a visit

A person's stay on the shop can be several recordings: the collector starts a new session after 30 minutes of inactivity, on sign-out and after an order. Opening any one of them shows the whole visit as a strip above the player, with one block per part laid out on the real clock — so a tab that was open while another was being used appears above it rather than after it, and a gap where the visitor was away stays a gap. Click a block to watch that part; when a part ends, the one that began after it starts automatically. A visit of a single part looks exactly as it always did.

A session is one browser's visit and is shared by every tab of that browser: a product opened in a second tab is part of the same visit, not a new session. Footage, however, is recorded per tab — each tab numbers its own segments from 0 — because a change recorded in one tab cannot be replayed onto the other tab's page. The player therefore offers a tab strip when a visit was recorded in more than one tab and plays one tab at a time; a long tab recording is split into parts (part 2, part 3) after 500 segments, still under the same visit. Clicks, errors and other markers belong to the whole visit and are shown on every tab's timeline.

The Platform checks for gaps in the sequence. If segment 2 arrives before segment 1, or a segment is missing entirely, the recording is flagged:

{
"sessionId": "abc-123",
"hasGaps": true,
"missingSegments": [1]
}

This hasGaps flag is displayed in the Dashboard with a warning, so you know the playback may be incomplete.


Which Recordings You See

Every recording carries an interest score (0–100), computed on the server from how much happened in the visit: how long it lasted, how many pages and clicks it produced, and whether anything of value occurred — a lead, an order, an error, a burst of rage clicks. Frustration raises the score rather than lowering it, because the visit where something went wrong is the one worth your minutes.

Visits that showed nothing are hidden by default. A single page, under ten seconds, no clicks, no error and no frustration is a mis-tap or a bot, and a list full of them reads as though the product found nothing. The list always prints how many it hid and offers a Show short visits control beside the row count, so the floor is never silent — if your recordings page is empty and the count is not zero, collection is working.

Two things are deliberately never hidden: a visit where an error or a rage click was recorded, whatever it scored, and a visit that has not been scored yet, which is an unknown rather than a zero.

Recordings hidden this way are also removed after 3 days instead of the usual 30 — see Storage.

A visit that produced no footage at all is a different case, and is counted separately. The session row is created the moment the recorder announces its first page, which is before the first fragment of footage has been packed and uploaded — so a visitor who leaves inside that second leaves a row naming a page and nothing to play. Those are hidden from the list with their own count, and removed after 24 hours: the only thing that could still turn one into a recording is a late upload arriving, which takes seconds, not days.

A visit is uploaded only once it has shown something. The recorder buffers the opening of every visit in the browser and sends nothing until one of four things happens: the visitor interacts (a click, a key, a tap), the page throws an error, the visit lasts 5 seconds, or your own code calls flushNow(). A visitor who opens a page and leaves within a second therefore costs no upload, produces no recording and uses none of your plan — while a visit that earns its upload at four seconds still arrives complete, because the buffer is held rather than discarded: the first segment you receive is still the opening of the visit.

This is only the FOOTAGE. Pageviews, clicks, scroll depth, leads and errors are separate collectors with their own transports, none of them gated this way — so a bounce is still counted, still attributed to its traffic channel and still visible in analytics. What it does not leave behind is a recording of nothing.

How long a visit lasted is measured when the visit ends, not when it starts. A recording is scored while it is still in progress, so the platform re-scores a visit a few minutes after it goes quiet, and immediately when the browser reports its final fragment. That is why a recording made moments ago may briefly show a shorter duration than it had — and why its score, which the floor above reads, settles within minutes rather than overnight.


Rage Clicks & Dead Clicks

Detected by their own collector, not by the recorder, and stored as their own rows rather than as a JSON blob on the session. That split is deliberate: a JSON array hanging off one session can tell you a person got frustrated, but it cannot answer where on the checkout people rage-click, which is the only version of the question worth asking.

It also means frustration data keeps working with session replay switched off — the interactions bundle carries no rrweb and costs a few KB.

Interaction typeDefinition
Rage click3 or more clicks within 800 ms, inside 30 px, on the same element
Dead clickA click after which nothing changes for 700 ms — no DOM mutation, no scroll, no navigation, no network request — and the element was rendering a pointer cursor

The cursor condition is what makes dead clicks a signal instead of noise. Without it every click on a paragraph counts as dead. With it, the statement is precise: the page showed this was clickable, someone took the offer, and nothing happened.

Positions are normalised in the browser before they are sent:

  • rx — a 0–1 fraction of the content root's width, not the viewport's. A click on the edge of a centred 1200px container sits at 0.26 of a 2560px screen and 0.03 of a 1280px one; storing viewport fractions smears every heatmap into the empty margins.
  • yBand — the offset down the content root in 20px bands. Vertical position is not a fraction of anything, because page height changes with content.
  • devicemobile / tablet / desktop / wide. These buckets never mix. The same CSS renders different layouts, so averaging across them draws a page nobody was served.

Clicks are also grouped by a page key — a hash of the normalised path, with numeric and id-like segments and product slugs collapsed. Without it a 10 000-SKU catalogue produces 10 000 heatmaps of three clicks each. The trade is stated plainly: a collapsed page is an average of its instances, which is right for layout questions and wrong for questions about one specific product.

rage and dead are annotations over the click stream, not extra clicks: every click is recorded once as click, and a burst or a silent click adds a second row describing it. Summing all three types double-counts — each is its own overlay.


Privacy Considerations

Every form input is masked — every <input> of every type, every <textarea> and every <select>, replaced character-for-character with * so an abandoned field still looks different from a completed one. Also never captured: cross-origin <iframe> content, canvas content, and any element carrying data-wpel-mask or the rr-ignore class.

What is NOT masked, and you should know this before enabling recordings: text your own pages RENDER. A customer's name in the header, a saved address on the account page, an order confirmation showing the basket and the delivery address — all of it is captured as the visitor saw it, because it is page text rather than an input value. That is normal for every session-replay product, and it is the reason data-wpel-mask and Settings → Privacy → Excluded URLs exist. Use them on any page that renders personal data.

The Privacy Policy states this in the same terms; if the two ever disagree, that one is authoritative.

You can add data-wpel-mask to any element you want excluded from recordings:

<div data-wpel-mask>This content will not be recorded</div>

Storage

By default, recording segments are stored on the Platform server filesystem.

Recordings are kept for 14 days, except those hidden as showing nothing (see Which Recordings You See) — those are removed after 3 days, files and rows together. Three rather than one because the nightly re-score revisits the last two days: a lead or an error can be matched to a visit after the footage arrives, and a recording must never be deleted before the job that could raise its score has seen it.

For high-volume sites, configure S3-compatible object storage — see Configuration → Storage.


Session Stitching

Multiple recording sessions by the same visitor (across different page loads) are linked under a single visitor profile. You can view the full chronological session history for a visitor in Dashboard → Visitors → [Visitor] → Sessions.

This allows you to trace a user's entire journey across multiple visits.