Pourquoi ta heatmap Matomo coupe apres une certaine ligne (et comment corriger ca)

Les captures d'ecran des heatmaps Matomo deviennent noires ou blanches sous une certaine ligne quand une sidebar, un panneau a onglets ou n'importe quel conteneur avec une hauteur limitee tronque son overflow pendant la capture. Les clics sont bien enregistres, ils atterrissent juste sur du vide. Voici pourquoi ca arrive et comment on gere ca.

Si tu as ouvert une heatmap Matomo et trouve tout ce qui se trouve sous une certaine ligne remplace par un rectangle noir, ta sidebar s'etant effondree a la hauteur qu'elle avait a l'ecran, avec des marqueurs de clics empiles inutilement sur de la couleur plate, les clics eux-memes vont bien. Matomo les a enregistres aux bonnes coordonnees. Ce qui est casse, c'est la capture d'ecran en dessous. Un conteneur de ta page en production qui limitait sa propre hauteur et laissait son contenu deborder a l'interieur d'une region scrollable (la sidebar, le panneau a onglets, le slide-over avec la FAQ) n'a pas charge son contenu complet dans le DOM capture. Le conteneur a garde sa hauteur limitee pendant la passe de capture, et Matomo a peint la zone au-dela comme de l'espace vide.

Le correctif le plus rapide est une extension Chrome gratuite qu'on maintient, qui s'appelle Matomo Heatmap Helper. Juste avant chaque capture, elle parcourt tous les elements de la page, trouve ceux dont le scrollHeight depasse le clientHeight, et reecrit leur height et max-height pour que la capture voie le contenu complet. Elle restaure les valeurs d'origine une fois la capture terminee. La suite de cet article explique comment faire la meme chose sans extension, plus un pattern CSS permanent qui vaut le coup d'etre mis en production.

Comment corriger ca sans l'extension

Il y a un snippet console qui colmate le probleme juste avant chaque capture, et un pattern CSS declenche par query param que tu peux mettre en production pour garder l'UX de tes vrais utilisateurs intact. Choisis celui qui convient a ta stack.

Lister les elements fautifs

Avant de changer quoi que ce soit, trouve les conteneurs qui tronquent le contenu. Ouvre la page dans Chrome, appuie sur F12 pour ouvrir les DevTools, passe sur la Console, colle ca, et appuie sur Entree. Tout ce qui est logue avec une difference de plus de cinq pixels environ est un candidat :

js
// Paste into the browser console on the affected page.
// Lists every element whose contents are clipped by a capped height.
Array.from(document.querySelectorAll('*'))
  .filter(el => el.scrollHeight > el.clientHeight + 1)
  .forEach(el => console.log(el.scrollHeight - el.clientHeight, el));

Les petites differences (un ou deux pixels) sont generalement des arrondis sous-pixel et ne sont pas ton probleme. Tout ce qui rapporte des centaines ou des milliers de pixels de contenu tronque, c'est la que ca se passe. C'est la que la heatmap s'assombrit.

Le correctif rapide : forcer l'expansion de chaque conteneur tronque

C'est le snippet qu'on colle dans la console du navigateur juste avant de declencher la capture de heatmap de Matomo. Il parcourt tous les elements de la page, etend ceux dont le contenu deborde, et desactive leurs regles d'overflow pour que le contenu complet soit visible. Au moment ou Matomo serialise le DOM, les sections tronquees sont deroulees.

js
// Paste into the console. Expands every container that has hidden overflow.
document.querySelectorAll('*').forEach(el => {
  if (el.scrollHeight > el.clientHeight + 1) {
    el.style.height = el.scrollHeight + 'px';
    el.style.minHeight = el.scrollHeight + 'px';
    el.style.maxHeight = 'none';
    el.style.overflow = 'visible';
  }
});

Pour confirmer que rien n'est encore tronque avant de declencher la capture :

js
// Returns the number of elements still clipping their contents. Should be zero.
Array.from(document.querySelectorAll('*'))
  .filter(el => el.scrollHeight > el.clientHeight + 1).length;

Si c'est zero, c'est bon. Si ce n'est pas le cas, les elements restants se trouvent generalement a l'interieur de shadow roots ou d'iframes cross-origin que le snippet ne peut pas atteindre. On en parle plus bas.

C'est le chemin rapide. Ca marche pour les captures ponctuelles et les cycles de review ou tu as besoin d'une capture propre maintenant sans attendre une modif de code. Pour les pages que tu recaptures regulierement, mets en place le correctif permanent ci-dessous.

Le correctif permanent : une feuille de style reservee a la heatmap

L'astuce consiste a garder l'UX de tes vrais utilisateurs intact (sidebars sticky, panneaux a onglets scrollables, tout ce qui existe pour une raison) tout en laissant la passe de capture voir le contenu complet. Une classe CSS declenchee par query param est la facon la plus simple de faire ca :

css
/* Edit the selectors below to match your site's containers */
html.heatmap-capture .sidebar,
html.heatmap-capture .tab-panel,
html.heatmap-capture [data-scrolls] {
  max-height: none !important;
  height: auto !important;
  min-height: 0 !important;
  overflow: visible !important;
}
js
// Add to your site. Turns the override on when you visit with ?heatmap=1
if (new URLSearchParams(location.search).get('heatmap') === '1') {
  document.documentElement.classList.add('heatmap-capture');
}

Maintenant, visiter https://your-site.com/page?heatmap=1 met la page en mode capture-friendly. Les vrais utilisateurs arrivent sur la page sans le param et gardent leur UX de scroll normal. Le meme site, deux mises en page, un query param entre les deux.

Si tu ne veux pas exposer le declencheur dans l'URL, remplace la verification du param par un cookie defini depuis une route admin-only, ou par une verification du User-Agent contre le fetcher de captures de Matomo (grep tes logs d'acces pour la chaine UA exacte qu'utilise le renderer de Matomo, puis filtre cote serveur). Meme idee, verrou different.

Le compromis

Ne mets pas max-height: none pour tous les visiteurs. Les sidebars sticky, les panneaux a onglets scrollables et les drawers a hauteur limitee existent pour une raison : ils gardent la navigation visible, ils permettent aux panneaux de coexister avec le reste de la mise en page, ils evitent qu'un accordeon de 4 000 pixels de haut prenne toute la page. Tout l'interet de filtrer l'override est de garder la production telle que les vrais utilisateurs la connaissent et de ne basculer la mise en page que pendant la capture.

Workflow teste : ouvre la page dans une fenetre incognito avec ?heatmap=1, clique un peu pour enregistrer la session de heatmap, laisse l'onglet ouvert jusqu'a ce que la capture de Matomo se declenche. Les vrais utilisateurs sur la meme page dans leurs sessions normales ne voient jamais l'override.

Pourquoi Matomo ne peut pas capturer au-dela de la coupure

La capture d'ecran de Matomo est un processus en deux etapes. D'abord, le tracker serialise ton DOM en direct en HTML et l'envoie. Ensuite, ton serveur Matomo re-rend ce HTML pour produire la capture que tu vois dans la vue heatmap. Le renderer peint la page exactement telle que le DOM serialise la decrit. Si un conteneur dans ce DOM a height: 600px et overflow: hidden avec un contenu qui fait en realite 2 000 pixels de haut, seuls les 600 premiers pixels entrent dans l'image. Tout ce qui se trouve en dessous est tronque a la limite du conteneur, et le canvas de la heatmap peint la zone au-dela comme de l'espace vide (noir, blanc ou transparent selon ce qu'il y a en dessous).

Les coordonnees des clics sont traquees au niveau du document, donc elles survivent sans probleme au tronquage. Les pixels qu'ils visaient, non.

Ce qui echoue vraiment

Quelques patterns qu'on croise regulierement :

  • Les sidebars avec max-height: calc(100vh - header) et overflow-y: auto, classiques pour les mega-nav, les filtres facettes et les panneaux de chat. La sidebar est plus haute que le viewport sur la page en production. La capture ne voit que la tranche a hauteur de viewport.
  • Les panneaux a onglets ou chaque [role=tabpanel] a son propre conteneur de scroll, comportement par defaut dans Bootstrap, Tailwind UI, MUI et la plupart des bibliotheques de composants. L'onglet actif est tronque a sa hauteur limitee et les onglets inactifs ne sont pas visibles du tout.
  • Les sections de type drawer qui ne sont pas vraiment des modales : slide-overs, tiroirs de filtres etendus, panneaux de panier inline. Tout ce qui est limite a 100vh ou une hauteur en pixels fixe expres. Le conteneur reste limite pendant la capture.
  • Les cartes de dashboard avec des scrollers internes, des tuiles a hauteur fixe ou le contenu deborde dans un div overflow-y: auto. Matomo capture la tuile a ses dimensions exterieures et tout ce qui se trouve au-dessus de la tranche visible du scroller interne disparait.
  • Les bibliotheques de composants qui integrent ca : ScrollArea de shadcn/ui, le Drawer de MUI avec scroll interne, les listes de combobox de Tailwind UI, les panneaux a onglets de Headless UI. Ca vaut le coup de le savoir quand tu audites la page, l'element fautif est souvent trois couches en dessous dans un wrapper que tu n'as pas ecrit.
  • Les iframes aux dimensions limitees. Le DOM propre de l'iframe est tronque a la taille exterieure de l'iframe, et Matomo ne peut de toute facon pas acceder a l'interieur des iframes cross-origin.

Si ta heatmap s'assombrit apres une certaine ligne, tu touches au moins un de ces cas. La plupart des pages avec une sidebar de contenu en touchent deux a la fois.

C'est aussi la meme famille de problemes qui casse les polices, les images et les en-tetes sticky dans la meme capture. On a ecrit un article plus complet sur les captures d'ecran de heatmaps Matomo cassees qui couvre le reste, plus des articles separes sur pourquoi les polices ne se chargent pas dans ta heatmap Matomo et pourquoi les images ne se chargent pas dans ta heatmap Matomo.

Ce qu'on ferait vraiment

Si tu controles les templates, mets en production la feuille de style filtree. C'est une poignee de selecteurs et une verification de query param, et ca reste en production aussi longtemps que tu utilises les heatmaps Matomo. Cinq minutes de travail, ca tient indefiniment.

Si tu ne peux pas, le snippet console couvre le meme terrain pour les captures ponctuelles. Le probleme, c'est que tu dois penser a le coller avant chaque capture.

Si ni l'un ni l'autre n'est accessible (templates verrouilles par un CMS, widgets tiers que tu ne possedes pas, tout contexte ou tu ne peux pas ajouter une regle CSS ou une balise script), l'extension Chrome Matomo Heatmap Helper est ce qu'on utilise sur les sites clients ou on ne peut pas modifier la stack. Elle effectue la meme reecriture scrollHeight/maxHeight pendant la capture et restaure les originaux ensuite, plus des correctifs equivalents pour les polices, les images et les en-tetes sticky. Gratuit, open source, code sur GitHub.

Martez est le projet plus large dont l'extension est issue. Il connecte Matomo avec Meta Ads et Google Ads pour que le ROAS, la CLV et l'attribution se retrouvent a cote de tes analytics web plutot que dans un tableau de bord separe. Il est en beta privee. Rejoins la liste d'attente si c'est pertinent pour toi.

Quelques selecteurs et un query param. Ca vaut le coup de le faire une fois.

Pourquoi ta heatmap Matomo coupe apres une certaine ligne (et comment corriger ca) - Martez Blog