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.
Heatmaps
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
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.
Cells are percentages of the document, not pixels — pages of any length and layout stay comparable, with device-class + viewport dimensions on every cell.
Render over the live page in an iframe preview or over a stored snapshot, switching between the 375 / 768 / 1280 device-class presets.
A cell holds a count, not a visitor. No user identifier can be attached to a click — the grid is the point.
Layers
≥3 clicks in 500 ms within a 48 px radius — detected client-side, counted server-side. The frustration signature, visualized where it happens.
Clicks on non-interactive elements, marked per element — the fastest way to find buttons that look clickable but aren't.
Reached-% by depth band, fed by 25 / 50 / 75 / 100 bucket events — paired with per-page scroll-depth reporting.
Every layer renders over the live page (iframe preview) or a stored snapshot — same grid, same ramp, per device class.
Scroll depth
Four bucket events — 25 / 50 / 75 / 100% — feed a reached-% heatmap by depth band, so a long page stops hiding its cliff.
| Depth band | Reached | |
|---|---|---|
| 25% | 84% | |
| 50% | 63% | |
| 75% | 41% | |
| 100% | 22% |
Element ranking
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.
| Element | Clicks | Rage share | Dead share |
|---|---|---|---|
| [data-obs-id="cta-primary"] | 412 | 2% | 1% |
| a.nav-pricing hashed | 297 | 0% | 4% |
| [data-obs-id="hero-demo"] | 244 | 1% | 0% |
| .plan-card button hashed | 188 | 6% | 9% |
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
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.
No fingerprinting, no cross-site identity, no ad-platform integrations — the refusals are why the default tier needs no consent banner.
Rage and dead-click layers answer the same "where does interaction go wrong" question — without recording motion.
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.