Documentation menu

Website heatmaps

Heatmaps show click density, document scroll reach and two friction heuristics on geometry-only page overlays. These are masked wireframes of reviewed public regions, with numeric tables. They never fetch your website, screenshot a page, load assets or collect DOM text, selectors, URLs, input values, user IDs or session IDs. They are separate from retention-grid charts and session replay.

Set up one reviewed layout

Open Analytics → Heatmaps → Collection settings in the intended environment. Add a static page key (pricing), public display label (Pricing), layout version (v1) and approved element keys (hero, plans, start). Review these as public application metadata: no customer names, contact information, routes containing identifiers, CSS selectors or URL values. Save a reason with a recent passkey. Collection is disabled by default. Reads require analytics.read; changes require analytics.write. Management tokens can read reports but ingest keys cannot.

Install the optional entry beside your existing browser client:

import { createClient } from "@saaspro/browser";
import { startHeatmaps } from "@saaspro/browser/heatmaps";

const spm = createClient().init({
  key: "YOUR_PUBLIC_INGEST_KEY",
  host: "https://YOUR_CONTROL_PLANE",
  consent: "pending",
});

// Run after the reviewed page has finished rendering.
const heatmaps = startHeatmaps(spm, {
  page: "pricing",
  layout: "v1",
  elements: ["hero", "plans", "start"],
  consent: "pending",
  onDelivery: ({ accepted, outcome }) => console.log({ accepted, outcome }),
});

// Your consent manager grants the two corresponding choices separately.
spm.consent("granted");
heatmaps.setConsent("granted");

// Withdraw heatmap consent immediately when the visitor changes their choice.
// heatmaps.setConsent("revoked");

Mark each approved region once in the page. The SDK exports only the static key and quantized geometry, never the element's text, attributes or link destination.

<section data-spm-heatmap="hero">...</section>
<section data-spm-heatmap="plans">...</section>
<a data-spm-heatmap="start" href="/signup">Get started</a>
<div data-spm-heatmap-ignore>Private content</div>

The core browser bundle does not import this entry. It can be installed with the React client's underlying SpmClient as well. An initialized client accepts one heatmap controller; stop() is final for that controller until document reload.

Both separate heatmap consent and active persistent analytics consent are required. Ephemeral analytics, pending/revoked consent and absent sessions disable capture. Nothing is recorded retrospectively. Revocation aborts pending requests, cancels heuristic timers and drops the in-memory observation. An already accepted report remains until operator deletion or retention expiry. Granting consent again begins a new observation with a new random ID and zero prior counts.

Identity, session or collector-key changes discard pending work and suspend capture. Call startPage after the new reviewed page has rendered. SPA navigation also requires this call; paths and route history are never observed or exported. If the old page should be delivered, await flush() before changing the route. startPage() deliberately discards unsent old-page data.

heatmaps.startPage({
  page: "pricing",
  layout: "v2", // Configure this reviewed version on the server first.
  elements: ["hero", "plans", "start"],
});

Each observation fixes schema version 1, page/layout keys, document dimensions, 64 CSS-pixel viewport upper-bound buckets and 16 CSS-pixel region geometry. The server hashes that full structure. Only identical geometry identities are combined; a desktop and mobile layout or two dynamic page heights never become one hotspot. Device filters are viewport families, not hardware detection: mobile ≤768 px, tablet ≤1216 px, desktop above that, using bucket upper bounds.

A changed region, viewport or document size freezes the observation and marks its coverage as limited. It never moves old clicks onto new content. Start a fresh observation only after the replacement layout is stable. BFcache restoration begins a fresh observation if consent remains eligible. Observations expire after 15 minutes; further capture requires an explicit new page. They are sampled page observations, not unique visitors or a census of traffic.

What can be measured

Only unique approved elements of at least 16×16 CSS pixels are eligible. Inputs, forms, contenteditable areas, private/masked/ignored ancestors, embedded documents, custom elements, fixed/sticky or transformed ancestors, invisible regions and pinch-zoomed viewports are excluded. The SDK observes document scroll only; nested scrolling is unsupported and a resulting geometry change freezes capture. Use data-spm-heatmap-ignore, data-spm-private, data-spm-mask, or data-spm-ignore around sensitive regions. Closed shadow DOM is not traversed.

Trusted primary-pointer clicks are counted; keyboard/programmatic activation is excluded. Clicks on eligible elements land in a 10×10 relative grid. The report shows captured clicks beside all observed eligible-class pointer events, so uncaptured regions are visible as missing coverage without disclosing their text.

  • Rage heuristic: three clicks on the same approved region within 1.5 seconds and adjacent grid cells. Each disjoint group contributes three heuristic clicks.
  • No-response heuristic: clicks on approved non-control regions, with no DOM mutation or document scroll for one second while the page remains visible. Links, buttons, media and semantic controls are ineligible. At most 20 pending checks run at once. DOM mutations are counted only in memory; their contents never enter a payload. The eligible denominator includes interrupted checks.
  • Scroll reach: the deepest visible document edge, in 5% bands, relative to the fixed document height. Counts include the initial viewport. This measures reach, not attention, dwell time or proof that content was read.

Friction signals are heuristics, not proven frustration. Delayed responses, canvas updates, intentional repeated clicks and unrelated DOM mutations can produce false positives or false negatives. Review the counts and coverage before changing a product. No browser/network response instrumentation is used, so the collector cannot trigger its own observations.

Delivery, limits and deletion

The SDK sends cumulative reports to POST /api/v1/ingest/heatmaps, normally after five seconds of changed data, on explicit flush and on page hide. One request runs at a time; overlapping flushes coalesce. Requests use omitted credentials, reject redirects and time out after five seconds. A flush reports that attempt's receipt; it cannot retract a request already accepted by the server. Failure callbacks return bounded outcome labels and never response content.

An observation has at most 30 regions, 256 click cells, 10,000 observed clicks, 300 sequences and a 32 KiB UTF-8 body. Reaching a capture cap marks coverage as limited. Receipt bodies are capped at 2 KiB. Network/temporary failures retain only the current cumulative in-memory report; later activity or explicit flush can retry it. There is no persistent queue or unbounded background retry loop. 400/401/403/409/413/429 refusals and sampled-out receipts suspend capture. Correct the cause before explicitly starting another page; do not loop on a refusal.

The server requires a live scoped ingest key, enabled Analytics and collection policy, an allowed Origin for public browser traffic, and approved static keys. Bot public traffic is refused. Arrival must be within 24 hours. Same-sequence retries are idempotent; conflicting rewrites, changing geometry and decreasing counters are rejected. Later cumulative reports replace one observation.

Limits are per environment: configurable deterministic sampling (1–100%), 100–20,000 new observations per UTC day, 20,000 retained observations and 128 MiB of JSON payload (database overhead excluded). Usage shows accepted reports, duplicates, sampled attempts and budget refusals. Sampling rates are not used to inflate observed counts into estimated totals.

Retention is 1–30 days, capped by application retention. Shortening it hides older observations immediately; a fair worker checks up to 50 scopes every five minutes and deletes bounded batches. Pausing collection retains existing history. Delete one contributing observation with a reason, confirmation and fresh passkey; its 48-hour tombstone prevents late delivery from recreating it. The 48-hour window exceeds the 24-hour arrival limit; expired tombstones are purged. A hard 100,000-tombstone limit bounds deletion metadata separately from the payload budget; at capacity deletion refuses atomically until maintenance frees space. Application/environment hard deletion cascades the data. No person identifiers or memberships are stored, so these observations cannot be linked to or filtered by a person profile. Collection, settings, deletion and purge actions append scoped audit events.

Read the same report through the Management API:

GET /api/v1/apps/{app}/analytics/heatmaps?env=production&page=pricing&device=mobile&mode=clicks

The response includes up to 100 observed geometry identities, one selected layout, aggregated cells/scroll bands, coverage and 25 contributing observations. Use exact layout, viewport (for example 384x896), days, and offset filters. Viewport and geometry are measured using the CSSOM View APIs; pointer eligibility follows Pointer Events.