Der Google Tag Manager (GTM) erleichtert das Erfassen von Conversions, da außer der einmaligen Implementierung des Codes keine Codeanpassungen direkt auf der Website vorgenommen werden müssen. Für eine Vielzahl an Interaktionen mit der Website gibt es vordefinierte Listener (Formulare, Klicks…), welche ohne umfangreiche Programmierkenntnisse integriert werden können. Jedoch ist beispielsweise das Tracking eines Formularversands auch mit dem Google Tag Manager (GTM) nicht immer ganz einfach. Probleme mit GTM internen Listenern gibt vor allem beim Einsatz von Ajax basierten Elementen.

Ajax Listener für den Google Tag Manager

Der Google Tag Manager (GTM) erleichtert das Erfassen von Conversions, da außer der einmaligen Implementierung des Codes keine Codeanpassungen direkt auf der Website vorgenommen werden müssen. Für eine Vielzahl an Interaktionen mit der Website gibt es vordefinierte Listener (Formulare, Klicks…), welche ohne umfangreiche Programmierkenntnisse integriert werden können. Jedoch ist beispielsweise das Tracking eines Formularversands auch mit dem Google Tag Manager (GTM) nicht immer ganz einfach. Probleme mit GTM internen Listenern gibt vor allem beim Einsatz von Ajax basierten Elementen.

Ajax Formulare & Google Tag Manager

Mit Hilfe von Ajax (Asynchronous JavaScript and XML) ist es möglich HTTP-Requests an einen Server zu schicken, ohne dass die Seite neu geladen werden muss. Genau dieses „Nicht-Neuladen“ der Seite verursacht dem GTM Probleme, da der Listener die Änderung des Element- bzw. Seitenzustandes nicht erfassen kann. Da die meisten modernen Websites Ajax Elemente beinhalten, ist es trotz des GTM oftmals nicht möglich Conversions korrekt zu erfassen.

Kontaktformulare sind essentielle Conversionpunkte auf Dienstleistungswebsites, weshalb ein exaktes Tracking unerlässlich ist. Wenn Formulare auf Ajax basieren, verursachen genau diese Elemente Probleme beim Conversiontracking.

Für ein korrektes Conversiontracking von Formularen ist es jedenfalls nicht sinnvoll:

  • auf Ajax Elemente gänzlich zu verzichten
  • an wichtigen Conversionpunkten überhaupt keine Conversions zu erfassen
  • jeden Klick auf den Absenden-Button eines Formulars zu erfassen

Beim Einsatz von Ajax-basierten Formularen ist es mittels des GTM internen Formularübermittlungs-Listener nicht möglich, das korrekte Absenden des Formulars zu erfassen. Der „Submit“ des Formulars wird vom GTM nie erfasst und eine dahinterliegende Conversion daher auch nie ausgelöst.

Vermeintliche Lösungsansätze wie das Abhaken der Checkbox „Bestätigung überprüfen“ bewirken nur, dass der GTM bei jedem Klick auf den Absenden-Button ein Submit-Event auslöst.

Submit Trigger
Screenshot

Fehlerhafte Versuche des Formularversands durch Validierungsfehler werden dadurch nicht mehr ausgefiltert. Wird ein Formular also vom Besucher zweimal fehlerhaft ausgefüllt und erst beim dritten Versuch korrekt abgeschickt, werden auch vom GTM drei Submit-Ereignisse erfasst. Ebenso werden dahinterliegende Conversionpunkte auch dreimal ausgelöst.

Ajax Listener: Die Lösung für das Tracking von Ajax-basierten Elementen

Um die Problematik von Ajax-basierten Elementen zu umgehen, haben wir folgenden GTM Listener entwickelt. Unser Code schickt im Rahmen eines Callbacks beim Success-Event des XMLHttpRequests ein Event dataLayer.push({‘event’: “ajax_request”}); an den GTM. Dieses Event sowie die übergebenen Datenlayerwerte können im GTM dann als Trigger für einen beliebigen Tag verwendet werden. Ebenso ist der Ajax Listener vollkommen unabhängig von anderen Libraries (z.B. jQuery), sodass die Einbindung dieser nicht berücksichtigt werden muss.

Einbau des Ajax Listeners

Mit Hilfe eines Custom HTML Tags kann der Code für den Ajax Listener integriert werden:

  • Tag Name: Beliebig z.B. Custom JS – Ajax Listener
  • Produkt auswählen: Benutzerdefiniertes HTML-Tag
Tag für den Ajax Listener
Screenshot
  • Tag konfigurieren:
<script>
var both_ajax2gtm = function ()
{
    var STRICT_MODE = false;    //set true when strict is enabled. Reduces functionality

    function parse_url_query(_query)
    {
        //console.log("AJAX - parse_url_query", _query);
        try
        {
            if(_query instanceof FormData)
            {
                parsed = {};
                var entries = _query.entries();
                var current_entry = entries.next();
                while(!current_entry.done)
                {
                    parsed[current_entry.value[0]] = current_entry.value[1];
                    current_entry = entries.next();
                }
                return parsed;
            }

            if(typeof(_query) === "undefined" || _query == null)
            {
                return {};
            }
            var parsed = {};
            var entities = _query.split('&');
            for(var i = 0; i < entities.length; ++i)
            {
                var entity = entities[i].split("=");
                parsed[decodeURIComponent(entity[0])] = decodeURIComponent(entity[1])
            }
            return parsed;
        }
        catch(_e)
        {
            return {};
        }
    }

    function Request_Logger()
    {
        function push_to_layer(_data, _req)
        {
            //console.log("AJAX - push_to_layer");
            var data = _data;
            data.statusText = _req.statusText;
            data.status = _req.status;
            data.readyState = _req.readyState;
            data.response = _req.response;
            try
            {
                data.response = JSON.parse(data.response);
            }
            catch(_e)
            {}
            if(typeof(window.dataLayer) === "undefined")
            {
                window.dataLayer = [];
            }

            if(data.caller_object == null)
            {
                var form_elements = document.getElementsByTagName('FORM');
                for(var fi = 0; fi < form_elements.length; ++fi)
                {
                    var base = form_elements[fi].baseURI.substr(0, form_elements[fi].baseURI.lastIndexOf("/")+1);
                    //console.log("[Matcher] base", base);
                    //console.log("[Matcher] action", form_elements[fi].action);
                    //console.log("[Matcher] shortaction", form_elements[fi].action.substr(base.length, data.url.length));
                    //console.log("[Matcher] url", data.url);
                    if(form_elements[fi].action.substr(base.length, data.url.length) == data.url)
                    {
                        var ajax_caller = {};
                        ajax_caller.certain = false;
                        ajax_caller.type = form_elements[fi].tagName;

                        for (var i = 0; i < form_elements[fi].attributes.length; ++i)
                        {
                            ajax_caller[form_elements[fi].attributes[i].name] = form_elements[fi].attributes[i].value;

                        }
                        data.caller_object = ajax_caller;
                        break;
                    }
                }
            }


            dataLayer.push({'event': "ajax_request", 'data': data});
            //console.log({'event': "ajax_request", 'data': data});
        }

        this.prepare_request_logging = function(_caller, _open, _send, _req)
        {
            //console.log("AJAX - prepare_request_logging");
            var data = {caller_object: _caller, method: null, url: null, get: null, post: null};

            var method = _open[0];
            var open_url = _open[1];

            data.method = method;

            if(open_url.indexOf("?") != -1)
            {
                data.url =  open_url.substr(0, open_url.indexOf("?"));
                data.get = parse_url_query(open_url.substr(open_url.indexOf("?") + 1));
            }
            else
            {
                data.url = open_url;
                data.get = {};
            }
            data.post = parse_url_query(_send);
            //console.log("---");
            //console.log("AJAX - PRE LAYER PUSH");
            //console.log(data);
            //console.log(_req);
            //console.log("---");
            _req.addEventListener("loadend", function(){push_to_layer(data, _req)});
        };
    }

    function init()
    {
        var logger = new Request_Logger();

        var old_open = XMLHttpRequest.prototype.open;
        var old_send = XMLHttpRequest.prototype.send;
        var open_arguments = null;
        var ajax_caller = null;

        XMLHttpRequest.prototype.open = function()
        {
            //console.log("AJAX - OPEN");
            try
            {
                var call_stack = arguments.callee;
                while(call_stack.caller != null)
                {
                    call_stack = call_stack.caller;
                }
                if(STRICT_MODE)
                {
                    throw "Strict Mode is active!";
                }
                var event_caller = call_stack.arguments[0].target;
                while (event_caller.tagName != "HTML" && event_caller.tagName != "FORM")
                {
                    event_caller = event_caller.parentNode;
                }

                if(typeof(event_caller.tagName) !== "undefined")
                {
                    ajax_caller = {};
                    ajax_caller.type = event_caller.tagName;
                    ajax_caller.certain = true;

                    for (var i = 0; i < event_caller.attributes.length; ++i)
                    {
                        ajax_caller[event_caller.attributes[i].name] = event_caller.attributes[i].value;
                    }
                }

            }catch(_e)
            {
                ajax_caller = null;
            }
            open_arguments = arguments;
            old_open.apply(this, arguments);
        };

        XMLHttpRequest.prototype.send = function()
        {
            //console.log("AJAX - SEND");
            logger.prepare_request_logging(ajax_caller, open_arguments, arguments[0], this);
            old_send.apply(this, arguments);
        };

    }
    window.addEventListener('load', init);
}();
</script>
  • Auslösen bei beliebig z.B. bllen Seiten, wenn der Listener auf jeder Seite Ajax Elemente erfassen soll

Schickt der Server die Antwort an den Browser, werden die Parameter und die Antwort des Ajax Requests zusammen an den Datenlayer übergeben.

Testen des Ajax Listeners

Ist der Tag gespeichert, kann dieser im Vorschau Modus des GTM getestet werden. Sobald der Server die Antwort auf den Ajax Request zurückschickt, wird im GTM das Event ajax_request ausgelöst.

Ajax Listener in der Google Tag Manager Vorschau
Screenshot

Bei Aufruf des Events werden diverse POST und GET Parameter in den Datenlayer geschrieben.

Ajax Listener Werte im Datenlayer
Screenshot

Je nach Formular werden unterschiedliche Werte im Datenlayer dargestellt. Basierend auf diesen Werten können Trigger für das Conversiontracking angelegt werden.

Verwendung des Ajax Listeners als Trigger

Auf einer Website können natürlich mehrere Ajax Request basierend auf beliebigen Elementen abgesetzt werden. Nicht immer soll jedes Ajax Event auch als Trigger eines Tags genutzt werden. Im Datenlayer gibt es meist einzigartige Werte, wie z.B. Formular Klasse oder ID o.ä., welche einen Trigger weiter eingrenzen können.

In unserem Fall möchten wir eine AdWords Conversion auslösen, wenn ein ganz bestimmtes Formular abgesendet wird.

1. Tag anlegen

  • Name: Beliebig z.B. AdWords – CT – Kontaktformular
  • Produkt auswählen: Google AdWords
  • Tag-Typ auswählen: AdWords-Conversion-Tracking
  • Tag konfigurieren: Conversion-ID & Label hinterlegen
AdWords Conversion Tag
Screenshot

2. Trigger anlegen

Nun muss noch ein Trigger angelegt werden. Basierend auf diesem Trigger wird die AdWords-Conversion ausgelöst.

Unter Auslösen wird der Punkt Mehr ausgewählt und danach ein neuer Trigger erstellt.

  • Name: Beliebig z.B. Event equals ajax_request
  • Ereignis auswählen: Benutzerdefiniertes Ereignis
  • Auslösen bei: Ereignisname: ajax_request

Mit dieser Konfiguration würde bei jedem ajax_request Event auch gleichzeitig eine AdWords-Conversion ausgelöst werden. Dies möchten wir nicht, da es auf unserer Beispiel-Website neben dem Kontaktformular noch weitere Ajax Elemente gibt. Deshalb fügen wir einen Filter hinzu.

Hierzu erstellen wir eine neue Variable, welche Daten aus dem Datenlayer beinhalten soll. Basierend auf dieser Variable kann der Filter definiert werden.

Benutzerdefiniertes Ereignis
Screenshot

3. Variable

  • Name: Beliebig z.B. ajax_request – form_id
  • Typ auswählen: Datenschichtvariable
  • Variable konfigurieren: data.post._form_id
Datenlayer Variable
Screenshot

Prinzipiell kann jeder beliebige Wert aus dem Datenlayer in eine Variable ausgelesen und dann für einen Trigger verwendet werden.

Datenlayervariable Vorschauansicht
Screenshot

In der Variable ajax_request – form_id steht der Wert sobald der Datenlayer ausgeführt wird – in unserem Fall 1.

Diese Variable kann jetzt als Filter für den Trigger verwendet werden:

Conversion Trigger
Screenshot

Angelegt ist also ein AdWords Conversion Tag: Dieser Tag wird gefeuert, wenn das GTM Ereignis ajax_request ausgelöst wird und die Datenschichtvariable ajax_request – form_id den Wert 1 beinhaltet. Dadurch wird garantiert, dass nur ganz bestimmte Ajax Requests als AdWords Conversion gezählt werden.

Unser Ajax Listener ist schon bei diversen Websites erfolgreich im Einsatz, wobei wir keine Funktionalität gewährleisten. Bei Problemen und Bugs würden wir uns über eine Rückmeldung freuen, da wir natürlich bestrebt sind, unseren Code stetig zu verbessern.