MATEUSZ POPŁAWSKI
The LabToolsO mnieBlogCertyfikatyKontaktUmów konsultację

Usługi

  • Performance Ads
  • Web Analytics
  • Landing Pages
  • Automatyzacja

Case Studies

  • The Lab

Wiedza

  • Blog
  • Certyfikaty

Tools

  • Growth Ops Tools
  • Kalkulator SST

Kontakt

  • O mnie
  • Umów konsultację

Mateusz Popławski

Growth Ops Architect

Design i wykonanie: Mateusz Popławski
(MatPop Digital Mateusz Popławski)

© 2026. Built for Scale.

Stack: Next.js 16, React, Tailwind, Framer Motion

Polityka Prywatności
Stape Partner - oficjalny partner platformy Server-Side GTMGoogle Partner Badge - certyfikat partnerstwa Google Ads
  1. Start
  2. The Lab
  3. content_read_complete: Koniec z vanity metrics w analityce contentu.
Mateusz Popławski

content_read_complete: Koniec z vanity metrics w analityce contentu.

content_read_complete: Koniec z vanity metrics w analityce contentu.

Executive Summary

Kluczowe informacje

  • 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.

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ę.

Sprawdź, ile konwersji tracisz. Kalkulator SST.

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:

  1. 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.prose był 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 niepewnego setTimeout, 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.

  2. 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 w window istnieje już instancja mojego observera i jeśli tak – wykonuje na niej twarde .disconnect().

  3. 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).

TrueReader.js
javascript
<script>
(function() {
  var MAX_RETRIES = 10;
  var RETRY_INTERVAL = 500;

  if (window.trueReaderObserver) {
      try { window.trueReaderObserver.disconnect(); } catch(e) {}
      window.trueReaderObserver = null;
  }
  window.trueReaderHasRead = false;

  var retryCount = 0;
  var waitForContent = setInterval(function() {
    var contentBlocks = document.querySelectorAll('[class*="prose"]');

    if (contentBlocks.length > 0) {
        clearInterval(waitForContent);
        initTrueReader(contentBlocks);
    } 
    else if (retryCount >= MAX_RETRIES) {
        clearInterval(waitForContent);
    }
    retryCount++;
  }, RETRY_INTERVAL);

  function initTrueReader(contentBlocks) {
    var startTime = new Date().getTime();
    var allText = "";

    contentBlocks.forEach(function(block) {
      allText += block.innerText || block.textContent;
    });

    var wordCount = allText.trim().split(/\s+/).length;

    if (wordCount < 50) return;

    var estTime = Math.ceil((wordCount / 225) * 60);
    var threshold = estTime * 0.25;

    window.trueReaderObserver = new IntersectionObserver(function(entries) {
      entries.forEach(function(entry) {
        if (entry.isIntersecting && !window.trueReaderHasRead) {
          var currentTime = new Date().getTime();
          var timeSpent = (currentTime - startTime) / 1000;

          if (timeSpent >= threshold) {
            window.trueReaderHasRead = true;
            window.dataLayer.push({
              'event': 'content_read_complete',
              'time_spent': Math.round(timeSpent),
              'est_total_time': estTime,
              'article_title': document.title
            });
            window.trueReaderObserver.disconnect();
          }
        }
      });
    }, { threshold: [0.1] });

    var lastBlock = contentBlocks[contentBlocks.length - 1];
    if (lastBlock) {
        var target = lastBlock.lastElementChild || lastBlock;
        window.trueReaderObserver.observe(target);
    }
  }
})();
</script>

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_complete oznacza 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".

Zrzut ekranu z Google Analytics 4 DebugView potwierdzający poprawne działanie niestandardowego zdarzenia "content_read_complete". Obraz pokazuje, że zdarzenie zostało poprawnie zarejestrowane wraz z kluczowymi parametrami: "estimated_time" (czas szacowany) wynoszącym 158 oraz "reading_time" (czas faktyczny) wynoszącym 69. Dane dotyczą artykułu o tytule "GTM Custom Data Attributes: Analityka odporna na zmiany UI", a typ ruchu "internal" wskazuje na pomyślny test wewnętrzny.
GA4 DebugView: Walidacja zdarzenia 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 lokalne var znikną z pamięci podręcznej funkcji, ale Listener na DOM zostanie.

  • Zmienne vs Treść: Pamiętaj, że innerText pobiera też tekst ukryty (np. w zwiniętych akordeonach). Jeśli masz dużo ukrytej treści, Twoja estymacja czasu może być zawyżona.

Chcesz podobnych rezultatów?

Umów bezpłatną konsultację i sprawdź, jak mogę pomóc Twojemu biznesowi.

Umów konsultację

Powiązane case studies

Automatyzacja przesyłu leadów do CRM zablokowanego na ruch przychodzący
Branża Automotive

Automatyzacja przesyłu leadów do CRM zablokowanego na ruch przychodzący

Zobacz case study
Server-Side Tagging  w branży Premium Automotive. Spadek CPA o 32%
Dealer Automotive (Segment Premium)

Server-Side Tagging w branży Premium Automotive. Spadek CPA o 32%

Zobacz case study
Jak Micro-copy podniosło jakość leadów z 15% do 45% (SQL).
Branża Automotive

Jak Micro-copy podniosło jakość leadów z 15% do 45% (SQL).

Zobacz case study