Skip to content

analytics: observeAll re-observa elementos já disparados a cada mutação do DOM — loop infinito de eventos view #516

Description

@viniciusventura29

Versão: presente em @decocms/blocks@7.51.0 e idêntico em 7.55.0 (mais recente).
Arquivo: src/sdk/analytics.ts, dentro de ANALYTICS_SCRIPT.

O bug

var viewObserver = new IntersectionObserver(function(entries) {
  entries.forEach(function(entry) {
    if (entry.isIntersecting) {
      var event = getEvent(entry.target);
      if (event) dispatch(event);
      viewObserver.unobserve(entry.target);   // desobserva...
    }
  });
}, { threshold: 0.5 });

function observeAll() {
  document.querySelectorAll("[data-event-trigger='view']").forEach(function(el) {
    viewObserver.observe(el);                  // ...mas re-observa tudo de novo
  });
}
observeAll();
var mo = new MutationObserver(observeAll);
mo.observe(document.body, { childList: true, subtree: true });

unobserve marca o elemento como já disparado, mas nada persiste esse estado. Qualquer mutação no body chama observeAll(), que re-observa todos os elementos — inclusive os que já dispararam. Se o elemento ainda está na viewport, o IntersectionObserver notifica de novo e o evento dispara de novo.

E o dispatch faz window.dataLayer.push(...), que faz o GTM disparar tags, que injetam nós no body, que geram mutação, que chama observeAll(). O ciclo se auto-alimenta.

Impacto medido

Loja VTEX real (deco-sites/farmrio-storefront), PLP, navegador parado, sem scroll nem clique:

GTM ativo GTM bloqueado
long tasks em 20s 245 tarefas, 15887 ms bloqueados 6 tarefas, 458 ms
chamadas a observe() 2340 108
exceptions não tratadas 133 1
clique num card → PDP 1146 ms 209 ms

79% da main thread consumida em loop, numa página parada. O view_item_list e o view_category dispararam 175 vezes cada em 30 segundos (deveriam disparar uma vez), cada um levando junto um TypeError: Cannot read properties of undefined (reading 'split') de dentro do gtm.js.

Além do custo do loop, o observeAll roda um querySelectorAll no documento inteiro a cada mutação — nesta PLP são 611 elementos com data-event-trigger, e 1181 callbacks de mutação em 20s.

Efeito colateral relevante: como o evento é re-enviado centenas de vezes, os relatórios de analytics ficam inflados — view_item_list reportado 175x por pageview.

Proposta

Persistir o "já disparou" no próprio elemento e excluí-lo do seletor:

    if (entry.isIntersecting) {
      var event = getEvent(entry.target);
      if (event) dispatch(event);
      entry.target.setAttribute('data-event-fired', '');
      viewObserver.unobserve(entry.target);
    }
...
function observeAll() {
  document.querySelectorAll("[data-event-trigger='view']:not([data-event-fired])")
    .forEach(function(el) { viewObserver.observe(el); });
}

Sugiro também agrupar o observeAll do MutationObserver num requestAnimationFrame/debounce — mesmo corrigido o loop, um querySelectorAll de documento inteiro por mutação é caro em página com muita tag de terceiro.

Feliz em mandar PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions