analytics: observeAll re-observa elementos já disparados a cada mutação do DOM — loop infinito de eventos view
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 5
- Forks
- 2
- Avg merge
- 20h 12m
- Merged PRs (30d)
- 36
Description
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.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/sdk/analytics.ts at ANALYTICS_SCRIPT, especially the IntersectionObserver callback and observeAll triggered by the MutationObserver. Persist the fired state on view elements and exclude them from later observation; verify that DOM mutations no longer redispatch an event for an element that has already intersected.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- analytics
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100