---
name: cursor-plugin-posthog-assessing-heatmaps
description: >-
  Assesses what a page's heatmap is telling you and recommends concrete changes. Pulls click / rageclick / scroll-depth data for a URL, names the hot elements by cross-referencing autocapture events on the same page, and can create a saved heatmap the user opens in PostHog, then summarizes the behavior and proposes improvements. TRIGGER when: user asks what a heatmap shows, why people aren't clickin
metadata:
  version: "0.1.0"
---

# Assessing heatmaps

A heatmap answers "where do people interact with this page?" — clicks, rage clicks, mouse movement, and how
far down they scroll. The data is pure geometry: `pointer_relative_x` (0..1 across the viewport), `pointer_y`
(absolute pixels down the page), and a count per spot. **It does not know what was clicked.** Turning
"lots of clicks at (0.5, 220)" into "lots of clicks on the Pricing nav link" is the whole job, and it comes
from cross-referencing autocapture on the same URL.

## Core principle: coordinates + meaning

You can't see the page — there is no screenshot in your context. A good assessment fuses two sources and
leans on autocapture to supply the layout/identity you can't see:

1. **Heatmap data** — where interactions land and how far people scroll (`heatmaps-list`).
2. **Autocapture** — what element sits under the hot spots, by element text / selector on the same page. This
   is what turns coordinates into meaning; without it you only have dots.

When the user wants to _see_ the heatmap, create a saved heatmap (Step 4) — that renders the page with the
data overlaid for them to open in PostHog. You reason from the data; they look at the picture.

## The flow

### Step 1: Pin the page and window

You need an exact `url_exact` (one page) or a `url_pattern` (regex, to aggregate across query strings). Confirm
the URL with the user if ambiguous. Default to the last 7 days; widen to 30 if volume is low. Heatmap data is
retained for 90 days.

### Step 2: Pull the data

Call `heatmaps-list` once per signal you care about (or query the `heatmaps` table directly via SQL — see the
querying-posthog-data skill, `models-heatmaps`):

- `type: "click"` — the primary "what draws attention" map.
- `type: "rageclick"` — repeated frustrated clicks. **The single strongest "something is broken or
  misleading" signal.** Any meaningful rageclick cluster deserves a callout.
- `type: "scrolldepth"` — how far people get. Use it to find the fold and spot CTAs that sit below where most
  people ever scroll.

Use `aggregation: "unique_visitors"` when you care about how many people (not how many clicks); `total_count`
exaggerates a few heavy clickers.

### Step 2b: Above the fold — read the `fold` summary

For the click types, `heatmaps-list` returns a `fold` object alongside `results`:

- `pct_below_fold` — share of non-fixed interactions that landed **below the user's initial viewport** (they
  had to scroll to reach them). This is one of the highest-value findings: content people actively click that
  sits below the fold is a prime candidate to move up.
- `below_fold_count` / `total_count` — the raw counts behind the percentage (fixed-position elements are
  excluded, since they're always on screen).
- `median_viewport_height` — the typical fold line in CSS pixels, to recommend against.

Report it concretely, e.g. "the fold is ~600px for most visitors, yet 35% of clicks land below it, so users
scroll before interacting — that content is a candidate for the first screen." **Segment by device** with
`viewport_width_min`/`viewport_width_max` (desktop and mobile have very different folds) and read `fold` per
band rather than blending them.

Need a distribution rather than a single percentage (e.g. clicks bucketed by how far below the fold)? Drop to
SQL on the raw `heatmaps` table, which has `y` and `viewport_height` in the same scaled units — see the
querying-posthog-data skill, `models-heatmaps`.

### Step 3: Name the hot elements (autocapture overlap)

For each notable cluster, find what's actually there. Query autocapture on the same URL — either via the
`exploring-autocapture-events` skill or directly:

```sql
SELECT properties.$el_text AS text, count() AS clicks
FROM events
WHERE event = '$autocapture'
  AND properties.$current_url = 'https://example.com/pricing'
  AND timestamp >= now() - INTERVAL 7 DAY
GROUP BY text
ORDER BY clicks DESC
LIMIT 25
```

`elements_chain` gives the selector/DOM path when you need to disambiguate two elements with the same text.
Match autocapture's top elements to the heatmap's hot coordinates: clicks concentrated on something that is
**not** a link or button (plain text, an image, a disabled control) is a classic "users expect this to be
clickable" finding.

### Step 4: Give the user a heatmap to look at (optional)

You can't see the page, but the user can. When a visual would help them follow your findings, create a saved
heatmap so they can open the rendered page with the data overlaid in PostHog:

1. `heatmaps-saved-create` with the page `url` (type defaults to `screenshot`). This enqueues a headless
   render — it is asynchronous. Pass `widths` matching the viewport band you analyzed in Step 2.
2. Poll `heatmaps-saved-get` (by the returned `short_id`) until `status` is `completed`, then tell the user
   it's ready to view in PostHog.

This is for the human's benefit — your own reasoning still comes from the Step 2 data and the Step 3
autocapture identity, not from the picture.

### Step 5: Drill into hotspots (when you need the "why")

For a surprising cluster, `heatmaps-events` returns the individual sessions behind specific `points`. Hand the
session IDs to the `investigating-replay` skill to watch what people actually did.

### Step 6: Summarize and recommend

Produce a short, concrete report:

- **What the heatmap shows** — top engaged elements, dead zones, scroll reach, and the above/below-the-fold
  click split (e.g. "viewport is ~600px for most visitors, yet 35% of clicks land below it").
- **Problems**, ranked by signal strength — rage-click clusters first, then clicks on non-interactive
  elements, then important CTAs sitting below the scroll cliff, then ignored primary actions.
- **Recommendations** tied to evidence — move/raise a CTA above the fold, make a clicked-but-dead element a
  real link, cut competing elements near a rage-click cluster, etc. Every recommendation should cite the
  signal it came from.

## Reading the signals

| Signal                             | Likely meaning                                                                    | Typical recommendation                                         |
| ---------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Rage clicks on an element          | Broken, slow, or looks-clickable-but-isn't                                        | Fix the handler, add feedback, or make it actually interactive |
| Many clicks on non-link text/image | Users expect it to be clickable                                                   | Make it a link/button, or remove the affordance                |
| Primary CTA gets few clicks        | Buried, low-contrast, or out-competed                                             | Raise it, increase contrast, reduce nearby noise               |
| Scroll cliff before key content    | Content/CTA is below where people stop                                            | Move it up or add a reason to scroll                           |
| High % of clicks below the fold    | Engaged content sits below the initial viewport — users scroll before interacting | Move the most-clicked elements onto the first screen           |
| Hot clicks on nav, cold body       | Page isn't delivering; people bail to nav                                         | Re-evaluate the page's core content                            |

## Gotchas

- **Heatmaps must be opted in** (`Team.heatmaps_opt_in`). If `heatmaps-list` returns nothing for a page that
  clearly gets traffic, capture may be off or the URL is wrong — check both before concluding "no
  engagement".
- **Coordinates are scaled** by a factor of 16 in storage; the API already returns CSS-pixel `pointer_y` and
  relative x, so use the API/tool values directly rather than the raw table columns.
- **You can't see the screenshot.** The saved-heatmap render is for the user to open in PostHog; don't claim
  to have looked at the page. Ground every layout claim in autocapture identity + coordinates, not vision.
- **Saved-heatmap rendering is async.** After `heatmaps-saved-create`, poll `heatmaps-saved-get` until
  `status` is `completed` before telling the user it's viewable. Only `screenshot`-type heatmaps render an
  image; `iframe` and `recording` types do not.
- **Mind the viewport.** A desktop click map and a mobile one are different pages' worth of behavior — filter
  with `viewport_width_min`/`viewport_width_max` rather than blending them.
