Z laboratoře vývojáře: Zákeřná a těžko debuggovatelná chyba v implementaci Data Layeru Google Tag Manageru

Jakub Kříž, Analytika, 3. 6. 2021

TLDR;

Do Data Layeru v GTM pushnu dva eventy za sebou a ten druhý se rozbije + řešení.

Pušnito do datalejeru!

Pušni to do datalejeru. Prostě tam napiš dataLayer.push a máš to tam. Pošli event na datalejer.

V mém světě to slyším a píšu každý den. Nebo skoro každý den už posledních 9 let. Koncepce Data Layer aka JavaScriptové pole je tuze elegantní. A zdomácněla. Je to srozumitelné, funky, trochu geeky a dobře to slouží.

Jenže celá tato elegance má jednu skrytou, nikde nedokumentovanou nedokonalost. Málokomu se stane a ještě méně lidí si ji kdy všimne. Ale jsou případy, hlavně u velkých implementací, kdy se to dříve či později stane. A když se to stane, je to spousta ztraceného času při debuggingu a opravování. V praxi se lze setkat s různými přístupy, jak tento problém odstranit, ale ne vždy je zvolené řešení vhodné. Což nikoho nemusí dlouho trápit, např. do doby, kdy má dojít k refaktoringu, nebo zvolená oprava způsobí stejný problém, jen později, protože nebyla pochopena její podstata.

A dá se tomu předcházet a to je moje motivace napsat tento článek.

Rovnou předestírám, že budu zacházet do hrůzných detailů Google Tag Manageru a jeho datové vrstvy, takže pokud si radši chcete osvojit koťátko, go here: https://www.kocicidomovslunicko.cz/kocky-k-adopci/ 

Takže o co jde?

Modelový případ. Na webu mám tlačítko pro sign-in do newsletteru Odeslat formulář. Ten je pro můj byznys zásadní a proto na něm chci mít měření analytiky.

Poprosím tedy svého vývojáře, aby mi na kliknutí na tlačítko navěsil event listener, který pošle do datové vrstvy event. Např.

dataLayer.push({
  event : 'formSubmit',
  formName : 'Newsletter Sign In',
  formLocation : 'Landing Page Free Book'
});

V GTM si krásně nastavíme Trigger typu Custom Event, na něj analytiku a jsme happy.

Semínko problému

O rok později někoho napadne, že by bylo dobré na celém webu měřit Engagement metriky nějak sofistikovaněji. Vytipujeme si místa, která podle nás svědčí o větším engagementu a jedno z nich bude tlačítko Odeslat formulář, protože člověk, co se přihlásil do newsletteru, jen podle nás engageovaný. 

Takže opět poprosím svého milého vývojáře, zda by byl tak laskav a navěsil event listener na tlačítko Odeslat formulář a poslal mi do datové vrstvy event. Např.

dataLayer.push({
  event : 'engagement',
  engagementType : 'Newsletter Sign In',
  engagementPoints : 5
});

Nastavíme GTM trigger, na něj Tag do analytiky a jsme happy. A taky jednou nohou v problému.

Kde je problém a proč si toho málokdo všimne?

Většina laických implementátorů kontroluje svoji implementaci podle doporučených nástrojů, jako je GTM Tag Assistant, nebo různé plužiny (zdravím Yuhů) do prohlížečů jako GTM Tag inspector či jiné berličky.

A tak správně vidíme dva eventy po sobě jdoucí.

Nejdříve formSubmit

Potom engagement

Dokonce nám debugger tvrdí, že je vše OK

V reálu se nám ale pošle něco jiného. K tomu ale potřebujeme vidět HTTP komunikaci. 

První event se pošle správně.

Druhý však vypadá nedorostle.

Je vidět, že se nepošlou custom parametry ep.engagementType a ep.engagementPoints. A to i přes to, že jsou korektně nastaveny v GTM.

Tak a proč je tomu tak?

Nejdříve se pojďme podívat na implementaci.

Ta běžně vypadá např. takto:

document.getElementById("formNewsletterSubmit").addEventListener("click",function(e){
    dataLayer.push({
        event : 'formSubmit',
        formName : 'Newsletter Sign In',
        formLocation : 'Landing Page Free Book'
    });

    dataLayer.push({
        event : 'engagement',
        engagementType : 'Newsletter Sign In',
        engagementPoints : 5
    });
  });

Takhle si naše zadání přeložil vývojář a pravděpodobně postupoval podle svého nejlepšího vědomí a svědomí. To že tady způsobil fatální chybu se dozví až za mnoho dlouho a je v tom neprávem, i když zde je jediné místo, kde to opravit.

Takže až se vám tohle stane, buďte na své vývojáře milí, oni za to nemůžou. Protože kdo to má vědět, že tohle nebude fungovat?

Proč to nefachá?

Uvnitř GTM při každém volání eventu na Data Layer dojde k dočasnému řekněme snapshotu aktuálního stavu Data Layeru jako celku aby během vykonávání Tagů nad daným Triggerem z dataLayeru nedocházelo k přepisování hodnot budoucími manipulacemi s Data Layerem vyvolaných dalšími asynchronní requesty. Data Layer je inkrementální, tedy s každým pushem se připíše a přepíše předchozí hodnota a nad ním se provede daný event, respektive trigger na něj navázaný. Díky onému snapshotu jsou zpracovávané tagy konzistentní.

Často jsou spouštěná pravidla pro analytiku a marketingové pixely velmi komplexní a může trvat stovky milisekund či jednotky sekund, než se celé sekvence vykonají. Pokud by se zpracovával synchronně, nebo též v tzv. blocking režimu, stránka by se na půl sekundy sekla a pak by jela dál. To je nepřípustné.

Proto GTM každý event zpracovává tzv. asynchronně. Jenže zde je problém, zakopaný hluboko v nitru.

JavaScript, ve kterém je GTM napsaný, nezná nic jako asynchronicitu. JavaScript (alespoň podle ECMA5) je synchronní, chcete-li jednovláknový. Takže se využívá fígle přes API browseru jménem setTimeout. Ten nám umožní kus kódu odložit bokem do paměti browseru a ten jej po požadovaném čase v milisekundách zařadí do řady ke zpracování. Tím se uvolní čas na další procesy. Pokud nic netrvá příliš dlouho, vše vypadá, že pracuje paralelně a všichni jsou happy. Pro představu by to šlo přirovnat k interrupcím v jádře systému na jednojádrových/jednovláknových procesorech.

Když se ale tzv. ztratí vlákno, tedy např. zapsáním bloku kódu do setTimeout API, tak původní script dále pokračuje ve zpracovávání sekvence.

A zde je jádro pudla. Nebo jádro trakaře. Jak chcete.

V rámci jednoho cyklu zpracování dataLayer.push() dochází k internímu snapshotu Data Layeru, který je v ten moment zamknutý pro změny. K odemčení dojde hned, jak je vlákno opuštěno, jenže to v našem anomálním případě nastane až po druhém dataLayer.push(), který si však do snapshotu nezapíše a ani se se svým snapshotem nevykoná.

Takže dokud se vlákno neuvolní po každém dataLayer.push(), ostatní v řadě mají smolíka.

Jak to řešit?

Základ je mezi dvěma a více voláními dataLayeru ztratit vlákno. 

Ilustrativně by se to dalo řešit následovně:

document.getElementById("formNewsletterSubmit").addEventListener("click",function(e){

    dataLayer.push({
        event : 'formSubmit',
        formName : 'Newsletter Sign In',
        formLocation : 'Landing Page Free Book'
    });

    setTimeout(function(){
       dataLayer.push({
          event : 'engagement',
          engagementType : 'Newsletter Sign In',
          engagementPoints : 5
       });
    },0);

  });

Tady je vidět, že druhý dataLayer.push() díky setTimeout ztratí vlákno a proto se již vykoná korektně.

A jak to dělat hezky?

To už je úkol pro programátora. Každý k tomu asi přistoupí trochu jinak a hodně záleží na celkové architektuře, jak je analytika implementována. 

Wrapper nad voláním dataLayeru

Pro místa s centrální konfigurací analytiky bývá vhodným způsobem vytvoření wrapperu, který se stará o postupné uvolňování zaslaných eventů na dataLayer.

Např něco ve stylu:

// protect internal GTM storage against flooding
var dataLayer = dataLayer || [];
var eventBuffer = [];
var emittingDelayMs = 1;
var eventQueue = {
  push : function(obj){
    eventBuffer.push(obj);
    processEventStorage();
  }
};
function processEventStorage(){
  if(processEventStorage.last > + new Date()-emittingDelayMs){
    setTimeout(function(){
      processEventStorage();
    });
  }
  else {
    processEventStorage.last = + new Date();
    dataLayer.push(eventBuffer.shift());
  }
}

V ten moment můžeme z jednoho event listeneru posílat dva dataLayer.push() bezpečně a to např následujícím způsobem.

document.getElementById("formNewsletterSubmit").addEventListener("click",function(e){
    eventQueue.push({
        event : 'formSubmit',
        formName : 'Newsletter Sign In',
        formLocation : 'Landing Page Free Book'
    });

    eventQueue.push({
          event : 'engagement',
          engagementType : 'Newsletter Sign In',
          engagementPoints : 5
      });    
  });

Jen jsme zde vyměnili dataLayer za eventQueue a tradá, máme po problému.

Samostatné event listenery

Stejného efektu dosáhneme i bindováním samostatných event listenerů pro každý dataLayer.push() samostatně.

document.getElementById("formNewsletterSubmit").addEventListener("click",function(e){
    dataLayer.push({
        event : 'formSubmit',
        formName : 'Newsletter Sign In',
        formLocation : 'Landing Page Free Book'
    });
  });

document.getElementById("formNewsletterSubmit").addEventListener("click",function(e){    
    dataLayer.push({
          event : 'engagement',
          engagementType : 'Newsletter Sign In',
          engagementPoints : 5
      });    
  });

Chápeme se? Já věřím, že ano!

To je celé, jen tohle mít na paměti, když implementujeme dataLayer.push() a jsme z obliga.

Já věřím, že to jednou Google Tag Manager vyřeší a tahle obezlička bude na smetišti dějin jako řada dalších vychytávek, ale i dnes, po 9 letech existence Tag Manageru, je to problém.

Problém jsme vyřešili, ale proč nám to neodhalil GTM Tag Assistant?

Je to prosté. Tag Assistant je de-facto samostatná implementace Tag Manageru, nebo lépe řečeno DataLayer Helperu a jeho konfigurace z gtm.json a v tomhle se prostě rozcházejí s Implementací v gtm.js. Jeden to dělá a druhý popisuje, jak to ten druhý dělá.

Pokud si chcete být jisti, že děláte svou práci implementátora opravdu správně, vždy se kromě doporučovaných debugging nástrojů koukejte i na následnou HTTP komunikaci, např. přes Charles Web Debugging Proxy, asi nejlepší nástroj svého druhu. Ale použít můžete i Debugging Tools integrované v každém prohlížeči, obvykle pod klávesou F12 a záložka Network.

Bez kontroly HTTP komunikace jsou jakékoliv závěry jen domněnky.

Tak hodně štěstí při implementaci Data Layeru a pokud s ním chcete pomoci, tak se neváhejte obrátit na Optimics. My ho známe do posledního šroubku.

Kuba

 

Co si přečíst dál?

Přidejte se do diskuze!

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *