Heatmaps

Where they click,
never who they are.

Click, scroll, rage and dead-click heatmaps stored as document-normalized aggregates on a 1%-width × 0.1%-height grid — rendered over the live page or a stored snapshot, with 375/768/1280 device-class presets. No user identifier can be attached to a click: that is the point of the grid.

The grid

Every click lands on a
1% × 0.1% cell.

Click coordinates are stored document-normalized and aggregated into grid cells 1% of document width × 0.1% of height, with device-class and viewport dimensions. Density renders as a blue→red ramp over the live page (iframe preview) or a stored snapshot — per device class.

Document-normalized

Cells are percentages of the document, not pixels — pages of any length and layout stay comparable, with device-class + viewport dimensions on every cell.

Live page or snapshot

Render over the live page in an iframe preview or over a stored snapshot, switching between the 375 / 768 / 1280 device-class presets.

Aggregate by construction

A cell holds a count, not a visitor. No user identifier can be attached to a click — the grid is the point.

Layers

Rage, dead clicks, scroll — each its own layer.

Rage-click layer

≥3 clicks in 500 ms within a 48 px radius — detected client-side, counted server-side. The frustration signature, visualized where it happens.

Dead-click layer

Clicks on non-interactive elements, marked per element — the fastest way to find buttons that look clickable but aren't.

Scroll heatmap

Reached-% by depth band, fed by 25 / 50 / 75 / 100 bucket events — paired with per-page scroll-depth reporting.

Live or snapshot

Every layer renders over the live page (iframe preview) or a stored snapshot — same grid, same ramp, per device class.

Scroll depth

How far they actually read.

Four bucket events — 25 / 50 / 75 / 100% — feed a reached-% heatmap by depth band, so a long page stops hiding its cliff.

Scroll heatmap · /blog/cookieless-analytics · 28 days4 buckets
Depth bandReached
25%84%
50%63%
75%41%
100%22%
Bucket events fire at 25 / 50 / 75 / 100% depth — aggregate counts, no identities.

Element ranking

Ranked by data-obs-id
pseudonymized anyway.

Top clicked elements are ranked by a stable data-obs-id — or, where you don't add one, by hashed selectors under the daily element key, so even DOM structure is pseudonymized in T0. Rage and dead-click shares are kept per element.

Top elements · /pricing · 28 daysby clicks
ElementClicksRage shareDead share
[data-obs-id="cta-primary"]4122%1%
a.nav-pricing hashed2970%4%
[data-obs-id="hero-demo"]2441%0%
.plan-card button hashed1886%9%
Selector hashes rotate with the daily element key — DOM structure is pseudonymized in the default tier.

The DOM contract: data-obs-id names an element for ranking, data-obs-interactive marks interactive targets for dead-click detection, data-obs-ignore excludes an element from heatmaps and forms.

By design

The layer we
left out.

There is no mouse-move heatmap in Observer. Movement traces are the least privacy-friendly signal in the category — the least insight per unit of surveillance — and the banner-free posture survives on what we refuse, not on what we promise.

What refusals buy

No fingerprinting, no cross-site identity, no ad-platform integrations — the refusals are why the default tier needs no consent banner.

What covers the gap

Rage and dead-click layers answer the same "where does interaction go wrong" question — without recording motion.

See where it breaks.
Never who broke it.

Heatmaps ship in the default tier with no consent banner — aggregate by construction, suppressed below the k-anonymity floor of n ≥ 5 on every view. Access is granted after an NDA, and deployment happens together with your DevOps team.