Cookieloses Tracking mit GA4

Published in Analytics, Datenschutz, Website-Tracking | 07.11.2021

Go to English version

Aufgrund seines neuen Datenmodells braucht ein cookieloses Tracking mit GA4 einige zusätzliche Konfigurationen, um richtig zu funktionieren. Anders als bei Universal Analytics ist diese Funktion nicht standardmäßig vorgesehen. In seinem Blog hat Mark Edmondson bereits einen Vorschlag gemacht, wie eine Lösung aussehen könnte. Dieser Ansatz ist jedoch nur begrenzt einsetzbar, da das GA4-Protokoll auf weiteren Parametern basiert, die Mark nicht berücksichtigt.

Im Folgenden will ich einen Ansatz vorstellen, der mithilfe des Consent-Modes, dem serverseitigen Google Tag Manager sowie einer über eine REST API erreichbaren Datenbank ein funktionales cookieloses GA4-Tracking ernöglicht.

Einleitung

Seitdem die Nutzung von Cookies durch Datenschutzgesetze und Browsereinstellungen immer weiter eingeschränkt werden, richtet sich der Blick zunehmend auf alternative Mechanismen oder gar der vollständige Verzicht auf Nutzeridentitäten. Was mit Universal Analytics noch relativ einfach zu bewerkstelligen war, ist mit GA4 schwieriger geworden, weil die dafür vorgesehene Einstellung im gtag-Skript nicht funktioniert.

Einen Ausweg bietet der neue Consent Mode, mit dem die Nutzung des Browserspeichers durch Analytics- oder Marketing-Tags zentral gesteuert und auf Basis der jeweiligen Nutzerzustimmung erlaubt oder verhindert werden kann. Allerdings entscheidet er auch darüber, ob Daten im Interface von Google Analytics überhaupt angezeigt werden. Daten, die ohne Einwilligung gesammelt wurden, werden von Google separat verarbeitet und mit künstlicher Intelligenz ausgewertet – aber nicht mit in die Datensammlung einbezogen. Mit dem serverseitigen Tag Manager können wir diese Einstellung jedoch verändern und so sicherstellen, dass alle Daten gesammelt werden.

Mit Consent-Mode und GA4 cookielos tracken

Der Consent Mode bietet vier Modi an, die für das Tracking relevant sind: denied und granted entscheiden jeweils für alle Analytics- oder Advertising-Tags, ob diese auf den Browserspeicher zugreifen dürfen. Mit diesem Modus kann verhindert werden, dass GA4 Cookies setzt.

Zu Beginn jedes Seitenaufrufs werden diese Modi festgelegt und in der dataLayer hinterlegt:

Consent Mode in der dataLayer

Ist der Consent Mode aktiv, wird den Google Analytics-Tags ein Parameter names gcs angehängt. Wurde der Nutzung des Browserspeichers zugestimmt, erhält dieser Parameter den Wert G111, andernfalls G100. Analog gilt: Jeder Tag mit dem Parameterwert G111 taucht in Google Analytics auf, während Tags mit dem Wert G100 von Google nachträglich für die Conversion-Attribution verwendet werden.

g/collect?v=2
&tid=G-ABCDEFGH
&gtm=2reb31
&_p=739263894
&sr=1680x1050
&gcs=G100                   // Consent-Value
&ul=de-de
&cid=GA123456.12314516
&_fplc=0
&ir=1
&_s=1
&dl=...
&dr=...
&sid=1636287718
&sct=1
&seg=0
&en=page_view
&_fv=1
&_ss=1
&_eu=Q

Um cookielos zu tracken, wollen wir ein Tag mit dem Parameterwert G100, das keine Cookies setzt und trotzdem vollwertig in Google Analytics dargestellt wird. Auf Basis eines von Mark Edmondson entwickelten Beispiels schauen wir uns an, wie das geht.

Serverseitige Verarbeitung des cookielosen GA4-Events

Der serverseitige Tag Manager bietet die Möglichkeit, jede eintreffende Anfrage nochmals zu bearbeiten, bevor sie an das eigentliche Ziel weitergeleitet wird. So können die Nutzerdaten zusätzlich geschützt werden, indem bspw. die IP-Adresse aus der Anfrage entfernt wird.

Auch die Einstellung des Consent Modes kann so nachträglich verändert werden, indem das Eventattribut x-ga-gcs einer GA4-Anfrage auf den Wert G111 verändert wird (credits an Mark Edmondson):

if (isRequestMpv2()) {
    // Claim the request
    claimRequest();

    const events = extractEventsFromMpv2();
    const max = events.length - 1;
    events.forEach((event, i) => {

      // Make unconsented hits appear in GA
      const consentMode = event['x-ga-gcs'];
      if (consentMode == "G100") {
        event['x-ga-gcs'] = "G111";
      }

      ...
}

Mit dieser Veränderung wird zunächst sichergestellt, dass eine GA4-Anfrage im Consent-Modus weiterhin in Google Analytics sichtbar ist. Der aufmerksamen Leserin wird jedoch nicht entgangen sein, dass sich dadurch außer einer vergrößerten Datenbasis noch keine gute Lösung ergibt: Jede GA4-Anfrage besteht jedoch aus einer Vielzahl von Attributen, die im Normalfall (ohne Consent-Modus) bereit im Client des Nutzer erhoben oder generiert werden. Dazu gehören:

  • Client ID (cid)
  • Session ID (sid)
  • Session Count (sct)
  • First Visit (_fv)
  • User Engagement (seg)

Einige dieser Parameter werden ebenfalls clientseitig gespeichert und abgerufen: Neben dem Cookie mit der Client ID _ga verwendet GA4 einen weiteren Cookie _ga_{Datastream ID}, der weitere Sitzungsdaten speichert. Auf Basis dieser Daten berechnet das GA4-Skript gtag, ob es sich um den ersten Besuch, eine neue Sitzung und um einen engaged user handelt.

Wenn GA4 keine Cookies setzt, werden diese Informationen bei jedem einzelnen Seitenaufruf neu generiert – jeder Aufruf erscheint als eine interaktionslose erste Sitzung eines neuen Besuchers. Es stellt sich somit die Frage, wie diese Informationen erhaltbar sind, ohne auf Cookies zurückgreifen zu müssen.

Damit nicht jedes weitere Event in Google Analytics einen neuen Nutzer generiert, ist eine vorübergehende Identität notwendig. Die Client ID markiert eine Nutzeridentität in Google Analytics und ist mithilfe eines gehashten Werts recht einach zu bestimmen. Ähnlich wie bei Matomo dienen dabei der User-Agent und die IP Adresse aus der Anfrage als Merkmale für einen Hash. Ein (tagesspezifischer) Salt macht diesen Hash zu einer temporären Nutzer-ID:

// Get User-Agent and IP from incoming request
const ua = getRequestHeader('user-agent');
const ip = getRemoteAddress();

// pick your own salt - can be anything
const salt = 'add a random sentence';

// Create a hashed ID from the IP and User Agent
var hash = sha256Sync(salt + ip + ua, {outputEncoding: 'hex'});

// Change the User Agent to the value from the request header and use server hash as client ID.
if(!event.ip_override) event.ip_override = "0.0.0.0";
if(!event.user_agent && ua) event.user_agent = ua;
if(hash) event.client_id = hash;

Mit diesem Hash können zumindest unterschiedliche Nutzer differenziert werden. Alle weitere der oben genannten Sitzungsattribute werden jedoch weiterhin mit jedem neuen Aufruf erneut generiert. Sie können auch nicht einfach durch einen Hash bestehender Informationen ersetzt werden. Hier braucht es einen anderen Ansatz, um diese Informationen vorübergehend zu erhalten, ohne sie im Browser des Nutzers abzuspeichern. Ein Beispiel für einen solchen Ansatz ist eine Datenbank, die über eine REST API serverseitig angefragt wird, um die jeweiligen Sitzungsattribute auf Basis eines reduzierten Informationsets zu ermitteln.

Sitzungsinformationen über Datenbank mit REST API ergänzen

Die Architektur des serverseitigen Tag Managers erlaubt es, nicht nur Anfragen an Google Analytics, Ads oder Facebook zu senden, sondern auch an eigene Endpunkte. Grundlage für dieses Vorgehen sind die beiden API-Funktionen sendHttpGet und sendHttpRequest.

Indem man einen Endpunkt bereitstellt, der die Informationen darüber erhält, in welchem zeitlichen Abstand ein Nutzer mit der Website interagierte und ob er überhaupt schon einmal dort war, können die Sitzungs ID, der Session Counter, das First-Visit-Attribut und das User-Engagement-Attribut ermittelt werden. Dafür ist eine Datenbank mit folgenden Feldern notwendig:

  • Letzter Timestamp (um die Zeit seit dem letzten Aufruf zu bestimmen)
  • Nutzerkennung (bzw. Server-Hash, um unterschiedliche Besucher auseinander zu halten)
  • Session ID
  • Session Counter

Steht eine solche Datenbank bereit, können vom ersten Aufruf an diese Informationen hinterlegt und aktualisiert werden, um die entsprechenden Attribute in GA4 anzupassen. Es empfiehlt sich, die Nutzerkennung als Primärschlüssel zu verwenden, um die Datenbank spezifisch danach abzufragen.

Anschließend werden drei unterschiedliche Zustände unterschieden:

  1. Der Nutzer war bereits auf der Seite und hat eine aktive Sitzung
  2. Der Nutzer war bereits auf der Seite und startet eine neue Sitzung
  3. Der Nutzer ist neu auf der Seite und startet seine erste Sitzung

1. Der Nutzer war bereits auf der Seite und hat eine aktive Sitzung

Eine Sitzung ist aktiv, wenn die Differenz zwischen dem letzten Timstamp und der aktuellen Zeit weniger als 30 Minuten beträgt. Bei diesem Zustand existiert der Nutzer bereits in der Datenbank und hat bereits eine Sitzungs-ID. Dadurch ist es zudem folgerichtig, dass es sich um einen engagierten Nutzer handeln muss. Entsprechend werden folgende Attribute aus der GA4-Anfrage modifiziert:

  • Die Parameter ga_session_id und ga_session_number erhalten den jeweiligen Wert aus der Datenbank
  • Der Parameter x-ga-mp2-seg (Engaged Session) wird von 0 auf 1 geändert
  • Die Parameter ['x-ga-system_properties'].fv (First Visit) und ['x-ga-system_properties'].ss (Session Start) werden entfernt

Daraufhin wird noch der aktuelle Timestamp in der Datenbank aktualisiert, dann ist der Vorgang beendet.

2. Der Nutzer war bereits auf der Seite und startet eine neue Sitzung

Wenn der letzte Timestamp länger als 30 Minuten zurückliegt, handelt es sich um eine neue Sitzung. Auch hier ist der Nutzer bereits in der Datenbank vorhanden. Entsprechend gilt:

  • Der Parameter ga_session_id wird aus der ursprünglichen Anfage übernommen.
  • Der Parameter ga_session_number wird mit dem jeweiligen Wert aus der Datenbank aktualisiert.
  • Der Parameter ['x-ga-system_properties'].fv (First Visit) wird entfernt

Anschließend wird der Timestamp, die Session ID und der Session Counter in der Datenbank aktualisiert.

3. Der Nutzer ist neu auf der Seite und startet seine erste Sitzung

Bei diesem Vorgang existiert noch kein Nutzereintrag in der Datenbank. Folglich erhält die Anfrage einen 500 Response Code und endet erfolglos.

Tritt dieser Fall ein, ist eine erneute Anfrage notwendig, die der Datenbank eine neue Zeile mit den erforderlichen Parametern hinzufügt.

Code für Datenbankabfragen

Alle Berechnungen und Abfragen zusammen ergeben folgenden Code:

const JSON = require("JSON");
const sendHttpGet = require("sendHttpGet");
const sendHttpRequest = require('sendHttpRequest');
const setResponseBody = require("setResponseBody");
const setResponseStatus = require("setResponseStatus");
const setResponseHeader = require('setResponseHeader');
const getTimestampMillis = require('getTimestampMillis');
const makeInteger = require('makeInteger');

// User Client ID for query 
const queryCid = hash; 

// Request URL where REST API can be reached
const requestUrl = '< URL of REST API >';

// Setting current time und default parameters which be updated
const currentTime = getTimestampMillis();
let newUser = false;
let engagedUser = false;

// Preparing Response Headers for query to REST API
setResponseHeader("content-type", "application/json");
setResponseHeader("access-control-allow-credentials", "true");
setResponseHeader("access-control-allow-origin", getRequestHeader("origin"));

sendHttpGet(requestUrl, (statusCode, headers, body) => {

    var responseBody;

    if (statusCode >= 200 && statusCode < 300) {

      responseBody = body;
      const userData = JSON.parse(body);

      // Calculating time difference between last timestamp und current time.
      var timeDiff = currentTime - userData.server_timestamp;

      // If less than 30 minutes, treat as active session
      if (userData.session_id && ((currentTime - userData.timestamp) < (1000*60*30))) {

        engagedUser = true;

        event.ga_session_id = userData.session_id;
        event.ga_session_number = userData.session_number;
        event['x-ga-mp2-seg'] = "1";
        event['x-ga-system_properties'].fv = null;
        event['x-ga-system_properties'].ss = null;

        let postBody = 
              {
                timestamp: currentTime
              };
        postBody = JSON.stringify(postBody);

        // Sends a POST request and nominates response based on the response to the POST
        // request.
        sendHttpRequest(hostname + queryCid, (statusCode, headers, body) => {
          setResponseStatus(statusCode);
          setResponseBody(body);
          setResponseHeader('cache-control', headers['cache-control']);
        }, {headers: {'content-type': 'application/json'}, method: 'PUT', timeout: 500}, postBody); // headers: {'content-type': 'application/json'},

      // If more than 30 minutes, treat as new session
      } else if (userData.session_id && ((currentTime - userData.timestamp) > (1000*60*30))) {

        event['x-ga-system_properties'].fv = null;
        let newSessionId = event.ga_session_id;
        let newSct = makeInteger(userData.session_number);
        newSct += 1;
        event.ga_session_number = newSct;

        let postBody = 
              {
                session_number: newSct,
                session_id: newSessionId,
                server_timestamp: currentTime
              };
        postBody = JSON.stringify(postBody);

        // Sends a POST request and nominates response based on the response to the POST
        // request.
        sendHttpRequest(hostname + queryCid, (statusCode, headers, body) => {
          setResponseStatus(statusCode);
          setResponseBody(body);
          setResponseHeader('cache-control', headers['cache-control']);
        }, {headers: {'content-type': 'application/json', "access-control-allow-credentials": "true"}, method: 'PUT', timeout: 500}, postBody); 

        } 

      }

      setResponseStatus(200);

    } else {

      // If response is 500, quit and change to new user

      responseBody = "{}";

      newUser = true;

      setResponseStatus(500);

    }

setResponseBody(responseBody);

if (newUser) {

  let newSessionId = event.ga_session_id;

  let postBody = 
      {
        cid: hash,
        session_id: newSessionId,
        session_number: 1,
        name: "",
        server_timestamp: currentTime
      };
  postBody = JSON.stringify(postBody);

  // Sends a POST request and nominates response based on the response to the POST
  // request.
  sendHttpRequest(hostname, (statusCode, headers, body) => {
    setResponseStatus(statusCode);
    setResponseBody(body);
    setResponseHeader('cache-control', headers['cache-control']);
  }, {headers: {'content-type': 'application/json', "access-control-allow-credentials": "true"}, method: 'POST', timeout: 500}, postBody); 
}

Innerhalb eines Client-Templates für den serverseitigen Tag Manager modifiziert diese Routine die GA4-Parameter eines cookielosen Tracking zu einer kohärenten GA4-Anfrage, die innerhalb der GA4-Oberfläche als qualitative Interaktion dienen.

Zusammenfassung

Mit diesem Setup sind die Grundlagen für ein cookieloses Tracking mit GA4 geschaffen: Beim Client wird ein GA4-Tag ausgelöst, das ein benutzerdefiniertes Client-Template auf dem Server verarbeitet und anonymisiert. Diese Einstellungen genügen zunächst, um in Google Analytics eine sitzungsbasierte Auswertung von Websitebesuchern durchzuführen. Dabei werden nicht mehr Nutzerdaten erfasst als unbedingt notwendig, um eine vorübergehende Nutzer- und Sitzungsidentität samt ihren GA4-Parametern über Engagement und Anzahl bisheriger Sitzungen bzw. erstem Besuch zu erhalten. Gleichzeitig werden persönliche identifizierbare Informationen wie die IP-Adresse von der Anfrage entfernt, so dass eine nachträgliche Verfolgung oder Profilbildung unmöglich ist.

Wird dieses Setup entsprechend ausgebaut, sind noch andere spannende Anwendungsfälle denkbar. Die serverseitige abfragbare Datenbank ermöglicht es beispielsweise, die Daten um zusätzliche Informationen anzureichern, die dort zuvor abgelegt wurden (Stichwort Data Enrichment). So kann in Eigenregie ein System implementiert werden, welches anderfalls nur durch kostenpflichtige Tools wie Snowplow oder Segment erreicht werden könnte.

Share post: Copied!

Weitere Posts zu diesem Thema