Koniec z vanity metrics w analityce contentu.

Podsumowanie
Cel: Zastąpienie śmieciowych danych (przypadkowe scrolle) metryką deep engagement w środowisku Single Page App.
Stack: GTM (Custom HTML), React/Vite, IntersectionObserver API, GA4.
Wynik: Eliminacja "fałszywych czytelników" i pełna higiena danych.
01 / 03
Wyzwanie
Dlaczego standardowy Scroll Depth w GTM to Vanity Metric?
Standardowe śledzenie Scroll Depth (90%) w GTM to w dzisiejszych czasach Vanity Metric. Użytkownicy scrollują do stopki, by sprawdzić linki, adres firmy lub po prostu "przelatują" wzrokiem (skanowanie), nie konsumując treści. Traktowanie scrolla jako "przeczytanie" fałszuje statystyki zaangażowania.
Problem ten drastycznie eskaluje w środowisku SPA (Single Page Application). Reactowa architektura strony sprawia, że standardowe triggery GTM wariują, a brak przeładowania strony (Hard Reload) powoduje, że timery i listenery z poprzednich podstron działałają w tle.
Opieranie decyzji contentowych o to, że "ktoś zescrollował stronę", to proszenie się o kłopoty. Na mojej stronie potrzebowałem twardych danych: kto faktycznie poświęcił czas na lekturę.
02 / 03
Rozwiązanie
Algorytm deep engagement oparty na Intersection Observer API
Zdecydowałem się na wstrzyknięcie logiki przez GTM (Custom HTML Tag), aby nie nie zmieniać kodu strony przy każdej zmianie parametrów algorytmu. Data Flow: GTM Trigger (virtual_page_view) -> GTM Custom HTML (Injection & Cleanup) -> DOM (React) -> Data Layer -> GA4.
Deep dive (Implementacja & Roadblocks)
Budowa rozwiązania w GTM dla aplikacji SPA wymagała obejścia kilku krytycznych przeszkód:
Asynchroniczny Contentful vs GTM: W architekturze Headless, treść artykułu nie jest częścią statycznego kodu HTML – jest dociągana z API Contentful już po załadowaniu aplikacji. Powodowało to krytyczny race condition: GTM inicjował skrypt w milisekundach, gdy kontener
div.prosebył jeszcze pusty lub w ogóle nie istniał w DOM. Standardowy skrypt kończyłby się błędem „undefined” lub zerową liczbą słów. Zamiast niepewnegosetTimeout, zaimplementowałem inteligentne oczekiwanie. Skrypt w pętli (co 500ms) "odpytuje" strukturę DOM o obecność treści z Contentfula. Dopiero w momencie, gdy React pomyślnie wyrenderuje dane z API, następuje "uchwycenie" tekstu (innerText), kalkulacja metryk i aktywacja obserwatora. Dzięki temu analityka jest odporna na opóźnienia sieciowe (latency) po stronie API CMS-a.SPA Memory Leaks (Zombie Observers): W SPA zmiana adresu URL nie czyści obiektu
window. Każde przejście na nowy artykuł odpalało tag GTM ponownie, tworząc nową instancjęIntersectionObserver, podczas gdy stary observer nadal nasłuchiwał w tle. Przy 5 przeczytanych artykułach miałem 5 działających w tle skryptów wysyłających zduplikowane eventy. Rozwiązanie: Dodałem logikę "Garbage Collector" na początku skryptu. Kod sprawdza, czy wwindowistnieje już instancja mojego observera i jeśli tak – wykonuje na niej twarde.disconnect().Matematyka "True Read": Zrezygnowałem ze sztywnego czasu. Czas potrzebny na przeczytanie jest dynamiczny (
WordCount / 225 words per minute). Warunkiem zaliczenia konwersji jest:Użytkownik dotarł do końca artykułu (Intersection Observer na stopce).
Upłynął minimalny czas (Threshold = 25% estymowanego czasu).
03 / 03
Wyniki
Twarde Dane: Velocity Index i eliminacja „fałszywych czytelników” w GA4
Wyeliminowanie Vanity Metrics: Skończyłem z "fałszywymi scrollami" w raportach.
content_read_completeoznacza teraz rzeczywiste zapoznanie się z materiałem.Velocity Index: Dzięki parametrom w DataLayer widzę, które artykuły są czytane szybciej niż standard (bardzo angażujące) lub wolniej (trudne/nudne).
Proof of Concept w GA4 Debug View.
Event content_read_complete zarejestrowany po spełnieniu warunków algorytmu. W tym przypadku estymowany czas czytania wynosił 158s. Użytkownik spędził 69s (43%), co przekroczyło zdefiniowany próg walidacji (25%), kwalifikując wizytę jako "content_read_complete".

Pro-Tips
Global Scope Pollution: Wstrzykując kod JS w GTM na stronach SPA, musisz przypisywać zmienne do
window(np.window._trueReaderObserver), aby mieć do nich dostęp przy kolejnym "wirtualnym przeładowaniu" i móc je wyczyścić. Zmienne lokalnevarznikną z pamięci podręcznej funkcji, ale Listener na DOM zostanie.Zmienne vs Treść: Pamiętaj, że
innerTextpobiera też tekst ukryty (np. w zwiniętych akordeonach). Jeśli masz dużo ukrytej treści, Twoja estymacja czasu może być zawyżona.
Usługa: Web Analytics →
Chcesz podobnych rezultatów?
Umów bezpłatną konsultację i sprawdź, jak mogę pomóc Twojemu biznesowi.


