9. März 2019
GTM’s Native Events abfangen und Contextual Data Markup hinzufügen
Holen Sie sich den ultimativen Hack, um HTML5-Datenattribute für Ihr Tracking-Setup zu nutzen. Sauberes Markup. Weniger CSS-Selektoren.
Kürzlich las ich einen brillanten Artikel des langjährigen Bloggers Yehoshua Coren. Während er zu Beginn des Artikels nur auf unterhaltsame Weise an die frühen Anfänge seiner Reise erinnerte, hat er mich aber dann gleich zu Beginn des Kapitels Contextual Data Markup voll erwischt.
Aber warum? Anhand vieler Beispiele aus dem täglichen Leben des Analytics Profis erklärt er sorgfältig, wie sinnvoll es ist, Daten- HTML-Attribute zu verwenden, um relevante Tracking Informationen vorzuhalten.
Ich werde hier nicht alle Beispiele neu formulieren, Sie sollten auf jeden Fall seinen Artikel lesen und selbst nachschauen.
Klingt nicht allzu spannend? Ist es aber. Es stimmt, die Verwendung von Daten-Attributen an sich ist nicht neu. Ich arbeite auch häufig mit Datenattributen.
Yehoshua plädiert jedoch dafür, Datenattribute auf die nächste Ebene zu bringen, indem man die nativen GTM-Ereignisse (man denke an Click Trigger, Visibility Trigger) so erweitert, dass alle Datenattribute aus dem semantischen Kontext des gtm.-Elements des Ereignisses in dem Moment gesammelt werden, in dem das Ereignis in den dataLayer geschoben wird.
Das heißt, alle Datenattribute, die an das gtm.element oder die an eines der Eltern des gtm.elements angehängt sind (also den DOM nach oben wandern), werden neben den eingebauten Variablen von GTM, wie elementId, verfügbar sein.
Einen Momamt, das DOM hochwandern? Ja, falls Ihnen das bekannt vorkommt, könnte Sie das an Simo Ahavas "Capturing the correct element"-Methode erinnern. Wenn Sie mehr Attributtypen als nur die Datentypen erfassen möchten, können Sie den Code aus diesem Artikel entnehmen und ihn nach Ihren Bedürfnissen erweitern.
Auf diese Weise können Sie eine robuste Tracking-Grundlage aufbauen, die sich mehr auf implementierte Datenattribute als auf fragile CSS-Selektoren stützt.
Das winzige Problem mit Yehoshuas Artikel ist, dass er erläutert, wie wir eine großartige Lösung gebaut haben, aber er weigert sich, sie als Open Source zu veröffentlichen, sondern empfiehlt Ihnen, eine Lizenz von ihm zu kaufen. Was allerdings völlig fair ist. In dieser Branche gibt es viel Wissensaustausch, aber nicht alles kann ein kostenloses Buffet sein.
Genau in diesem Fall wollte ich jedoch eine ähnliche Funktionalität für mich selbst haben... aber auch selbst gebaut. Deshalb ist dieser Artikel ein Folgeartikel zu Yehoshua's, einschließlich des Codes. Nichtsdestotrotz ist das sogenannte Heisenberg-Plugin, für das er wirbt, sicherlich ein sehr schönes Stück Code, weshalb ich Sie dringend bitte, sich über seine Lizenzbedingungen zu informieren, auch wenn Sie mit dem Code aus diesem Artikel beginnen.
Genug Theorie für den Moment. Sie erhalten nun ein kurzes Inhaltsverzeichnis für den Rest dieses Artikels, bevor wir uns in praktisches Material stürzen, damit Sie wirklich verstehen können, was hier vor sich geht.
Ein E-Commerce-Beispiel inkl. aller Screenshots zum Verständnis des Nutzens
Der Implementierungscode, den Sie kopieren und einfügen können
Ausführliche und technische Erläuterung, wie der Code abgeleitet wurde
Jetzt geht's los.
Ein E-Commerce-Beispiel inkl. aller Screenshots zum Verständnis des Nutzens
Um ein kurzes Beispiel zu geben, werfen wir einen Blick auf die Demo der E-Commerce-Software Shopware, die eines der führenden Systeme für den deutschen Markt ist. Sie können die Demo leicht über eine Google-Suche finden.
So sieht derzeit eine Kategorieseite /beach-relax/accessoires/ in dieser Demo aus. Sie sehen die typische Produktrasterung, hier zwei Artikel pro Zeile.
Lassen Sie uns nun das (gekürzte) HTML-Markup für das Produktgitter überprüfen. Alles, was Sie hier beachten müssen, ist, dass Shopware bereits standardmäßig Datenattribute auf verschiedenen Ebenen des DOMs füllt. Es gibt also potentiell eine Menge zu tracken, noch bevor benutzerdefinierte Datenattribute hinzugefügt werden.
*<!-- First the product grid's container -->*
<div class=”listing” **data-ajax-wishlist**=”true” **data-compare-ajax**=”true” **data-infinite-scrolling**=”true” **data-loadprevioussnippet**=”Vorherige Artikel laden” **data-loadmoresnippet**=”Weitere Artikel laden” **data-categoryid**=”59" **data-pages**=”1" **data-threshold**=”4" **data-pageshortparameter**=”p”>
*<!-- Then child divs for all the product cards -->*
<div class=”product--box box--image” **data-page-index**=”1" **data-ordernumber**=”SW10414" **data-category-id**=”59">
[...]
*<!-- In the child divs we have links to the product detail pages, for example wrapping the image.. -->*
<a href=”[[](https://www.shopwaredemo.de/leichtes-tuch-taupe-hell-415?c=59)...]" title=”[...]” class=”product--image”>
<span class=”image--element”>
<span class=”image--media”>
<img srcset="[...] 2x” alt=”[...]” title=”[...]”>
</span>
</span>
</a>
*<!-- ... or the product title -->*
<a href="[[](https://www.shopwaredemo.de/leichtes-tuch-taupe-hell-415?c=59)...]" class=”product--title” title=”[...]”>
LEICHTES TUCH TAUPE HELL
</a>
[...]
</div>
[...]
</div>
Nehmen wir nun an, dass wir daran interessiert sind, Klicks auf Links innerhalb der Produktkarten zu verfolgen (eine typische E-Commerce-Aktion namens Produktklick).
Mit jedem Produktklick werden Sie eine Menge Informationen an Ihr Analytics senden wollen, nämlich Produktdetails und vielleicht andere interessante Datenpunkte.
Wie sieht angesichts des obigen HTML-Markups und eines einfachen Alle-Elemente-Klick-Auslösers im Tag-Manager das Ereignis aus, wenn auf den Link eines solchen Produktdetails geklickt wird? So sieht es aus:
Abgesehen von den eingebauten Tag-Manager-Variablen wie gtm.element
etc. nicht allzu viele Informationen, oder?
Und wie wird das gleiche Ereignis aussehen, wenn Sie den in diesem Artikel angegebenen Code anwenden? Schauen Sie es sich an:
Das ist eine Wucht! Neben den eingebauten Variablen haben wir eine weitere Eigenschaft namens dataContext, die alle Informationen enthält, die von data-Attributen gesammelt wurden, die beim Durchlaufen des DOM aufwärts vom gtm.element
gefunden wurden.
In diesem Shopware-Fall haben Sie plötzlich Informationen wie z.B., ob der unendliche Bildlauf aktiv ist oder nicht, direkt in Ihrem NATIVEN GTM-Klick-Ereignis.
Innerhalb des Tag-Managers können Sie nun einfach eine DataLayer-Variable "dataContext" hinzufügen und bei Bedarf auf alle ihre Eigenschaften zugreifen, wie in diesem Dummy:
Der Implementierungscode, den Sie kopieren und einfügen können
Ersetzen Sie den ursprünglichen GTM-Schnipsel durch die erweiterte Version, wie im nachstehenden Gist dargestellt:
/**
* THE STANDARD..
*
*/
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
/**
* ..BECOMES --->
*
*/
window.dataLayer = window.dataLayer || [];
window.dataLayer.pushStashed = window.dataLayer.push;
Object.defineProperty(window.dataLayer, 'push', {
set(y) {
this.pushStashed = y;
Object.defineProperty(this, 'push', {
writable: true,
value: function() {
arguments = [].map.call(arguments, function(event) {
if (event && event["gtm.element"]) {
event.dataContext = {}, elem = event["gtm.element"];
for (; elem && elem !== document; elem = elem.parentNode) {
[].forEach.call(elem.attributes, function(attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function($0, $1) {
return $1.toUpperCase();
});
event.dataContext[camelCaseName] = event.dataContext[camelCaseName] || attr.value;
}
});
}
}
return event;
});
return this.pushStashed.apply(null,arguments);
}
});
},
get() {
return this.pushStashed
},
configurable: true
});
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');
Detaillierte und technische Erklärung, wie der Code abgeleitet wurde
Wenn Sie daran interessiert sind, wie der obige Code funktioniert, werde ich versuchen, ihn in diesem Kapitel zu skizzieren.
Beginnen wir mit der Konzeptualisierung, d.h. mit der Frage, wie das Ziel erreicht werden kann.
Wenn wir darüber nachdenken, wie man mehr Informationen zu einem Argument hinzufügen kann, wenn dataLayer.push() aufgerufen wird, kommen wir zu dem Schluss, dass wir eine benutzerdefinierte Push-Funktion für die dataLayer-Variable schreiben müssen.
Wenn Sie jedoch window.dataLayer.push
nachschlagen, werden Sie sehen, dass die klassische Array.prototype.push
bereits durch die GTM-Bibliothek durch eine andere ersetzt wird.
Und unsere vorherrschende Bedingung ist, dass wir nicht (lies: SOLLTEN NICHT) einfach den minimierten GTM-Bibliothekscode ändern, da wir mit neuen Versionen der GTM-Bibliothek kompatibel bleiben wollen.
Doch unser Ausgangspunkt ist genau diese GTM-Bibliothek. Wenn Sie ein wenig beautifien, werden Sie auf dieses Stück Code stoßen:
ze = function() {
var a = La("dataLayer", []),
b = La("google_tag_manager", {});
b = b["dataLayer"] = b["dataLayer"] || {};
Tc(function() {
b.gtmDom || (b.gtmDom = !0, a.push({
event: "gtm.dom"
}))
});
qe(function() {
b.gtmLoad || (b.gtmLoad = !0, a.push({
event: "gtm.load"
}))
});
var c = a.push;
** a.push = function() {
var b;
if (0 < J.SANDBOXED_JS_SEMAPHORE) {
b = [];
for (var e = 0; e < arguments.length; e++) b[e] = new le(arguments[e])
} else b = [].slice.call(arguments, 0);
c.apply(a, b);
for (re.push.apply(re, b); 300 < this.length;) this.shift();
return xe()
};**
re.push.apply(re, a.slice(0));
A(ye)
};
Dies ist der Teil, in dem die GTM-Bibliothek unmittelbar nach dem Pushen des gtm.load-Ereignisses in die dataLayer-Variable die Push-Funktion des dataLayers (wie von Array.prototype geerbt) mit einer benutzerdefinierten überschreibt.
Was bedeutet das für unser Konzept? Es bedeutet, dass wir einen Weg finden müssen, die folgende Logik zu implementieren:
Warten Sie, bis GTM dataLayer.push außer Kraft setzt.
Speichern Sie dann sofort die Push-Funktion von GTM und ersetzen Sie dataLayer.push wieder durch unsere eigene benutzerdefinierte Funktion, die
zunächst unsere custom Daten verarbeitet und dann die gespeicherte GTM-Push-Funktion mit unserem Ergebnis aufruft.
Meine Lösung dafür ist der Code in diesem Artikel. Lassen Sie ihn uns schnell durchgehen:
Initiieren des dataLayer
Wir initiieren die Variable dataLayer und sichern bereits die Push-Funktion in der Eigenschaft pushStashed
window.dataLayer = window.dataLayer || [];
window.dataLayer.pushStashed = window.dataLayer.push;
Wir definieren dataLayer's Push mit Accessor-Deskriptoren
Das Schöne daran, vorübergehend einen Setter und Getter für die Push-Funktion zu definieren, ist, dass wir sie als eine Art Rückruf verwenden können, wenn GTM die Push-Funktion außer Kraft setzt.
Object.defineProperty(window.dataLayer, 'push', {
set(y) {
Was passiert, wenn GTM dataLayer.push ersetzt
Wenn GTM dataLayer.push jetzt außer Kraft setzen will, startet unser Setter mit der Variable y, die die benutzerdefinierte Push-Funktion von GTM enthält. Wir speichern diese Funktion dann in pushStashed. Dann definieren wir dataLayer.push neu und wechseln zurück zu den Datenbeschreibungen. Das heißt, dataLayer.push wird wieder einen Wert haben, der unsere benutzerdefinierte Funktion ist.
this.pushStashed = y;
Object.defineProperty(this, 'push', {
writable: true,
value: function() {
Unsere benutzerdefinierte Funktion
Von nun an wird unsere benutzerdefinierte Funktion aufgerufen, wenn irgendwo auf unserer Website dataLayer.push() aufgerufen wird. Die Funktion schluckt alle Argumente - für den Fall, dass mehrere Args übergeben werden, wie in dataLayer.push({Ereignis: 1},{Ereignis: 2}). Bei der Abbildung aller Argumente suchen wir nach der gtm.element -Eigenschaft in jedem davon. Wenn sie gefunden wird, durchqueren wir das DOM nach oben, sammeln dabei alle data-Attribute und füllen dataContext auf.
arguments = [].map.call(arguments, function(event) {
if (event && event["gtm.element"]) {
event.dataContext = {}, elem = event["gtm.element"];
for (; elem && elem !== document; elem = elem.parentNode) {
[].forEach.call(elem.attributes, function(attr) {
if (/^data-/.test(attr.name)) {
var camelCaseName = attr.name.substr(5).replace(/-(.)/g, function($0, $1) {
return $1.toUpperCase();
});
event.dataContext[camelCaseName] = event.dataContext[camelCaseName] || attr.value;
}
});
}
}
return event;
});
Überlassen Sie GTM den Rest
Schließlich übergeben wir unser Ergebnis an die Push-Funktion von GTM, die jetzt in dataLayer.pushStashed zu finden ist.
return this.pushStashed.apply(null,arguments);
}
});
},
get() {
return this.pushStashed
},
Obligatorisches
Vergessen Sie nicht, configureable:true zu setzen, wenn Sie zu Accessor-Deskriptoren wechseln, da dataLayer.push sonst nicht mehr neu definiert werden kann.
configurable: true
});
Abschließend kann ich nur noch sagen: Vielen Dank an Yehoshua Coren für die Idee. Diese neue Herangehensweise wird meine zukünftigen Tracking-Setups stark beeinflussen (hin zum Besseren).