Por que o heatmap do Matomo corta a partir de um certo ponto (e como corrigir)

As capturas de tela dos heatmaps do Matomo ficam pretas ou em branco abaixo de uma certa linha quando uma sidebar, painel com abas ou qualquer container com altura limitada corta o overflow durante a captura. Os cliques ainda são registrados, só ficam pousados no vazio. Veja por que isso acontece e como lidamos com isso.

Se você abriu um heatmap do Matomo e encontrou tudo abaixo de uma certa linha substituído por um retângulo preto, sua sidebar colapsada na altura que tinha na tela, e marcadores de clique empilhados inúteis sobre uma cor chapada — os cliques em si estão corretos. O Matomo os registrou nas coordenadas certas. O que está quebrado é a captura de tela embaixo deles. Algum container na sua página ao vivo que limitou a própria altura e deixou o conteúdo transbordar dentro de uma região com scroll (a sidebar, o painel de abas, o slide-over com o FAQ) não teve seu conteúdo completo incluído no DOM capturado. O container manteve a altura limitada durante o processo de captura, e o Matomo pintou a área além dele como espaço vazio.

A correção mais rápida é uma extensão gratuita para Chrome que mantemos chamada Matomo Heatmap Helper. Logo antes de cada captura, ela percorre todos os elementos da página, encontra os que têm scrollHeight maior que clientHeight, e reescreve o height e max-height deles para que a captura de tela veja o conteúdo completo. Ela restaura os valores originais quando a captura termina. O resto deste post explica como fazer a mesma coisa sem extensão, mais um padrão CSS permanente que vale implementar.

Como corrigir sem a extensão

Tem um snippet de console que resolve o problema logo antes de cada captura, e um padrão CSS ativado por query param que você pode implementar para manter a UX dos seus usuários reais intacta. Escolha o que melhor se encaixa no seu stack.

Liste os elementos problemáticos

Antes de mudar qualquer coisa, encontre os containers que estão cortando o conteúdo. Abra a página no Chrome, aperte F12 para abrir o DevTools, mude para o Console, cole o snippet abaixo e pressione Enter. Tudo que aparecer no log com uma diferença maior que cinco pixels é candidato:

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));

Diferenças pequenas (um ou dois pixels) geralmente são arredondamento de subpixel e não são seu problema. Qualquer coisa que reportar centenas ou milhares de pixels de conteúdo cortado é o que você está procurando. É aí que o heatmap está escurecendo.

A correção rápida: forçar a expansão de todos os containers cortados

Este é o snippet que colamos no console do navegador logo antes de acionar a captura do heatmap do Matomo. Ele percorre todos os elementos da página, expande os que têm conteúdo transbordando e desliga as regras de overflow para que o conteúdo completo fique visível. Quando o Matomo serializa o DOM, as seções cortadas já estão expandidas.

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';
  }
});

Para confirmar que nada ainda está cortado antes de acionar a captura:

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;

Se for zero, está ótimo. Se não for, os elementos restantes geralmente estão dentro de shadow roots ou iframes cross-origin que o snippet não consegue alcançar. Mais sobre isso abaixo.

Esse é o caminho rápido. Funciona para capturas únicas e os ciclos de revisão onde você precisa de uma captura limpa agora e não quer esperar uma mudança no código. Para páginas que você vai recapturar regularmente, implemente a correção permanente abaixo.

A correção permanente: um stylesheet só para o heatmap

O truque é manter a UX dos seus usuários reais intacta (sidebars fixas, painéis de abas com scroll, tudo que existe por um motivo) enquanto deixa o processo de captura do heatmap ver o conteúdo completo. Uma classe CSS ativada por query param é a forma mais simples de fazer isso:

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');
}

Agora, acessar https://seu-site.com/pagina?heatmap=1 coloca a página no modo amigável para captura. Usuários reais acessam a página sem o param e mantêm a UX normal de scroll. Mesmo site, dois layouts, um query param entre eles.

Se você não quiser expor o gatilho na URL, troque a verificação do param por um cookie configurado em uma rota só de admin, ou por uma verificação de User-Agent contra o fetcher de captura de tela do Matomo (grep nos seus logs de acesso pela string exata de UA que o renderizador do Matomo usa e então gate pelo lado do servidor). Mesma ideia, portão diferente.

O trade-off

Não implemente max-height: none para todos os visitantes. Sidebars fixas, painéis de abas com scroll e drawers com altura limitada existem por um motivo: eles mantêm a navegação à vista, permitem que painéis coexistam com o resto do layout, evitam que um acordeão de 4.000 pixels tome conta da página. Toda a razão de colocar um portão no override é manter o ambiente de produção do jeito que os usuários reais esperam e só mudar o layout quando você está capturando.

Fluxo testado: abra a página em uma janela anônima com ?heatmap=1, clique para registrar a sessão do heatmap, deixe a aba aberta até a captura do Matomo disparar. Usuários reais na mesma página nas sessões normais nunca veem o override.

Por que o Matomo não consegue capturar além do corte

A captura de tela do Matomo é um processo em duas etapas. Primeiro, o tracker serializa seu DOM ao vivo em HTML e envia para o servidor. Depois, seu servidor Matomo re-renderiza esse HTML para produzir a captura de tela que você vê na visualização do heatmap. O renderizador pinta a página exatamente como o DOM serializado descreve. Se um container nesse DOM tem height: 600px e overflow: hidden com conteúdo que na verdade tem 2.000 pixels de altura, só os 600 pixels do topo entram na imagem. Tudo abaixo é cortado na borda do container, e o canvas do heatmap pinta a área além dele como espaço vazio (preto, branco ou transparente dependendo do que estiver embaixo).

As coordenadas dos cliques são rastreadas no nível do documento, então elas sobrevivem ao corte sem problemas. Os pixels para os quais elas apontavam não sobrevivem.

O que está falhando de fato

Alguns padrões que continuamos encontrando:

  • Sidebars com max-height: calc(100vh - header) e overflow-y: auto, comum em mega-navs, filtros facetados e painéis de chat. A sidebar é mais alta que o viewport na página ao vivo. A captura de tela só vê a fatia com a altura do viewport.
  • Painéis com abas onde cada [role=tabpanel] tem seu próprio container com scroll, comportamento padrão no Bootstrap, Tailwind UI, MUI e na maioria das bibliotecas de componentes. A aba ativa é cortada na sua altura limitada e as abas inativas não ficam visíveis de forma alguma.
  • Seções estilo drawer que não são realmente modais: slide-overs, bandejas de filtros expandidas, painéis de carrinho inline. Qualquer coisa limitada a 100vh ou a uma altura em pixels fixos propositalmente. O container mantém a altura limitada durante a captura.
  • Cards de dashboard com scrollers internos, tiles de altura fixa onde o conteúdo transborda dentro de uma div overflow-y: auto. O Matomo captura o tile nas suas dimensões externas e tudo acima da fatia visível do scroller interno desaparece.
  • Bibliotecas de componentes que têm isso embutido: ScrollArea do shadcn/ui, Drawer do MUI com scroll interno, listas de combobox do Tailwind UI, painéis de abas do Headless UI. Vale saber quando você está auditando a página — o elemento problemático geralmente está três camadas abaixo de um wrapper que você não escreveu.
  • Iframes com dimensões limitadas. O próprio DOM do iframe é cortado no tamanho do iframe externo, e o Matomo não consegue acessar o interior de iframes cross-origin de qualquer forma.

Se o seu heatmap escurece após uma certa linha, você está enfrentando pelo menos um desses. A maioria das páginas com uma sidebar de conteúdo enfrenta dois ao mesmo tempo.

Esta também é a mesma família de problemas que quebra fontes, imagens e headers sticky na mesma captura de tela. Escrevemos um post mais completo sobre capturas de tela quebradas no Matomo heatmap que aborda o restante, além de posts separados sobre por que as fontes não carregam no seu Matomo heatmap e por que as imagens não carregam no seu Matomo heatmap.

O que faríamos de fato

Se você controla os templates, implemente o stylesheet com portão. São alguns seletores e uma verificação de query param, e isso fica em produção pelo tempo que você estiver usando heatmaps do Matomo. Cinco minutos de trabalho, dura para sempre.

Se não conseguir, o snippet de console cobre o mesmo terreno para capturas únicas. O problema é que você tem que se lembrar de colá-lo antes de cada captura.

Se nenhuma das opções for viável (templates bloqueados pelo CMS, widgets de terceiros que você não controla, qualquer situação onde você não pode adicionar uma regra CSS ou uma tag de script), a extensão Matomo Heatmap Helper para Chrome é o que usamos em sites de clientes onde não podemos mudar o stack. Ela executa a mesma reescrita de scrollHeight/maxHeight durante a captura e restaura os originais depois, além de correções equivalentes para fontes, imagens e headers sticky. Gratuita, open source, código no GitHub.

O Martez é o projeto maior do qual a extensão surgiu. Ele conecta o Matomo com o Meta Ads e o Google Ads para que ROAS, CLV e atribuição fiquem ao lado da sua análise web em vez de em uma planilha separada. Está em beta privado. Entre na lista de espera se isso for relevante pra você.

Alguns seletores e um query param. Vale fazer uma vez.

Por que o heatmap do Matomo corta a partir de um certo ponto (e como corrigir) - Martez Blog