How we deal with broken Matomo heatmap screenshots

Heatmap screenshots break on most modern sites because of how Matomo serializes and re-renders the DOM. Here are the patterns we kept running into, and a small open source extension we built to handle them.

If you've opened a Matomo heatmap and found a blank rectangle where your hero image should be, or a product grid that cuts off after the first row, or a sticky header sitting square on top of the page content, you're not the first. Heatmap screenshots break on most modern websites out of the box. The reason is structural, not something you fix by tweaking your tracking config.

TL;DR

Heatmap screenshots break on most modern sites, and it's a snapshot-and-replay problem, not a tracking one. Matomo stores the page structure and rebuilds it later, so cross-origin CSS, unstored images, and clipped or sticky elements fall apart in the preview even though your click and scroll data is fine. Matomo Heatmap Helper is an open source Chrome extension that handles all of that at capture time and then restores your live page. The part that earns its keep is Interactive mode, where you click the elements that misbehave so the extension knows what to fix; it needs Heatmap & Session Recording 5.1.0 or later and a Matomo API token with write access.

Matomo captures an initial page snapshot from the browser and stores the page structure for later heatmap viewing. CSS and images are not always stored with that snapshot; they may still be loaded from your site or CDN when Matomo reconstructs the page. Lazy content may not exist yet when the snapshot is taken. Viewport-dependent CSS may replay differently. A lot of things that worked because they were running in the original browser quietly fail when the heatmap viewer rebuilds the page later.

Once you see that, the bug list starts making sense.

The patterns we keep running into

Most sites hit at least three of these. Some hit all of them.

SymptomLikely causePermanent fixWhat the extension does
Missing styling, icons, or fontsCross-origin CSS or font files cannot be read during capture or replayAdd crossorigin="anonymous" on stylesheet links and return CORS headers for CSS/font filesFetches CSS and font resources with extension permissions and embeds what it can as data URIs
Broken hero or product imagesMatomo does not store images, so blocked, moved, lazy, or origin-sensitive image URLs can disappear laterKeep image URLs stable, make them absolute, and make sure they are loaded before captureResolves image URLs and embeds loaded images into the snapshot
Product grid or carousel cut offFixed-height overflow containers clip content even when it exists in the DOMAdd capture-only CSS that expands the container for heatmapsExpands selected scroll containers before capture, then restores them
Header covers the whole pageSticky/fixed headers, parallax sections, or 100vh wrappers replay differentlyScope heatmap-only CSS with html.matomoHeatmap and set explicit heights or static positioningConverts selected sticky/fixed elements to normal flow during capture
Assets referenced with relative URLs failURLs such as /images/hero.jpg are replayed outside the original page contextUse absolute URLs for heatmap-critical assetsRewrites relative URLs to absolute URLs before capture
SPA or lazy content missingMatomo's automatic snapshot ran before React, Vue, or lazy assets finished renderingUse Matomo's manual snapshot capture after the page is actually readyLets you wait, interact with the page, then trigger the manual snapshot

Most of these have infrastructure-level fixes. Add CORS headers on your CDN for CSS and font files. Switch important assets to absolute URLs. Use the html.matomoHeatmap CSS hook to override sticky positioning or height rules only during heatmap rendering. Matomo's own docs cover several of them, and we've written a longer post that walks through each issue and how to fix it server-side if that's the route you want.

The catch is that the work spreads across CDN config, CSS, and your tracking setup, and some of it you can't fix at all if you don't control the assets. We hit that wall on enough client sites that we ended up doing something else.

What we ended up building

We started with a small script we'd paste into DevTools before each capture. It embedded images and fonts as data URIs, expanded scroll containers, unstuck headers, rewrote relative URLs to absolute. Over time it grew into a Chrome extension we used internally on every client.

That's what Matomo Heatmap Helper is. We open sourced it.

Setup is one screen. You enter your Matomo URL, an API token with write access to the target site, and pick which sites you want the toolbar to appear on. On any matching page, a small bar shows up.

RequirementWhy it matters
Heatmap & Session Recording enabledThe extension uses Matomo's heatmap capture machinery; it does not add heatmaps to Matomo by itself.
Manual snapshot capture availableMatomo added the explicit manual snapshot workflow in Heatmap & Session Recording 5.1.0.
Token with write accessThe extension reads sites and heatmaps, updates the heatmap into manual capture mode, and verifies that a new snapshot exists.
A browser where the extension can runThe current extension targets Chrome and other Chromium-based browsers.
A page Matomo can trackThe Matomo tracker and Heatmap & Session Recording plugin still need to load on the page. If your IP is excluded in Matomo, Matomo's manual capture will not work until that exclusion is removed temporarily.

The work happens at capture time. When you press the screenshot button, the extension does the things that are time consuming to do statically. It embeds images and fonts as base64 data URIs where possible, so the heatmap snapshot is not depending on those assets being fetched again later. It expands scroll containers to their full content height. It converts sticky and fixed headers to normal flow. It rewrites relative URLs to absolute, resizes iframes, pauses videos to frame zero, and gives SPA content time to render. Then it enables Matomo's manual snapshot mode for the selected heatmap, triggers Matomo's HeatmapSessionRecording::captureInitialDom tracker command in the page, checks that Matomo received the result, and reverses every change. Your live page goes back to how it was.

The reason this works is that you can interact with elements before capture. Custom scroll containers, a misbehaving sticky header, a section where assets aren't loading: you click them in Interactive mode, they get a lock icon, and the pipeline knows what to do with them. The tracking script alone can't do that, because it captures whatever state the DOM happened to be in at the moment it ran. Letting a person tell the extension which elements need handling is what fixes the cases the script never could.

Issues and pull requests are welcome.

Why we put it out

As an agency that works deeply with Matomo, broken heatmaps were a problem we hit on almost every client engagement. The events were tracked correctly, but the screenshots were unusable, which is the part most stakeholders actually look at.

Matomo has given us a lot as an agency. We use the extension daily on client work, and we keep improving edge cases we find on client websites. If you run into an unresolved heatmap screenshot issue, open a GitHub issue with the affected URL pattern, Matomo version, browser, and a screenshot of the failure.

What we're working on

Matomo Heatmap Helper grew out of a larger project. Martez connects Matomo with Meta Ads and Google Ads, so ROAS, CLV, and multi-touch attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. If that's relevant to you, you can join the waitlist.

Either way, the heatmap extension is its own thing, and it stays that way. We built it because we needed it.