Sitecore Tracking – Umgang mit dem Cookie Consent

In den meisten Sitecore-Projekten kommt eine Cookie Consent Management-Platform (CMP) zum Einsatz. Aber wie in meinem ersten Artikel zum Sitecore Tracking bereits beschrieben bietet Sitecore keine OOTB-Lösung für unterschiedliches Tracking-Verhalten vor bzw. nach der Zustimmung eines Besuchers zum Sitecore Tracking Cookie.

Vor einer technischen Lösung gilt es im Projekt zu klären, was vor der expliziten Zustimmung zum Sitecore Tracking durch den Besucher überhaupt erlaubt ist. Dabei kristallisierten sich für mich drei Varianten heraus:

1. Tracking=None: Vor der Zustimmung erfolgt kein Tracking

Der Vorteil liegt auf der Hand: kein Tracking, kein Verstoß ;). Aber es stehen auch weniger Analytics Daten zur Verfügung um das Surfverhalten der Besucher auszuwerten und darauf basierend die User Experience und/ oder Journey zu optimieren.

2. Tracking=InSession: Vor der Zustimmung erfolgt ein Tracking der aktuellen Session (ohne Persistierung der Tracking-Daten in der Analytics Datenbank)

Bei dieser Variante ist zumindest eine Personalisierung auf Basis des aktuellen Besuches möglich (z.B. hat Seite X schon aufgerufen, dann wird Seite Y angeteasert). Die oben aufgeführten Nachteile aufgrund der fehlenden historischen Tracking Daten bleiben allerdings bestehen.

3. Tracking=Anonym: Bereits vor der Zustimmung wird der Besucher anonym getrackt.

Damit stehen die oben vermissten umfangreichen Analytics Daten zur Verfügung. Es gibt auch Stimmen, die das je nach Projekt als DSGVO konform einstufen würden, aber hier gilt immer die Prämisse, die Daten müssen dann wirklich genutzt werden.

Welche dieser Varianten im konkreten Projekt erlaubt/ gewünscht ist, das ist dann natürlich mit den Datenschutzverantwortlichen im Detail abzustimmen. Mit Blick auf den maximalen Nutzen der Sitecore Experience Platform ist die optimale Lösung das anonyme Tracking und die zweitbeste Lösung das InSession-Tracking 🙂
Egal wie die Entscheidung am Ende ausfällt, ich möchte im Weiteren gerne die technischen Lösungsansätze dahinter mit euch teilen. Denn es gibt zwar einige Quellcode-Schnipsel dazu im Internet, aber ich habe selbst nichts Ganzheitliches gefunden und bin noch in die ein oder andere Falle getappt.

Gemeinsamkeit aller Tracking-Varianten

Bevor wir uns den Lösungsansätzen der unterschiedlichen Varianten widmen, möchte ich zunächst mal auf die Gemeinsamkeiten eingehen:

Persistierung Cookie Consent

Alle Lösungsansätze setzen voraus, dass man jederzeit Kenntnis darüber hat, ob der Besucher seine Zustimmung zum Tracking bereits gegeben hat oder nicht. Üblicherweise wird dieser Status in einem permanenten Cookie vorgehalten. Wenn man jetzt aber einfach davon ausgeht, dass solange der Cookie (noch) nicht existiert, die Zustimmung nicht erteilt wurde, dann führt das in Hintergrund-Prozessen manchmal zu Fehlentscheidungen. Daher haben wir uns entschieden, in der ensureSessionContext-Pipeline einen weiteren Processor einzufügen, welcher den Cookie-Inhalt zusätzlich in die Session dupliziert:

namespace Comspace.Sitecore.Usercentrics.Pipelines.EnsureSessionContext
{
    public class EnsureTrackingConsent : InitializeTrackerProcessor
    {
        public override void Process(InitializeTrackerArgs args)
        {
            var consentCookie = HttpContext.Current.Request.Cookies[Settings.TrackingConsentCookieName];
            if (consentCookie != null)
            {
                if (args.Session.CustomData.ContainsKey(Settings.TrackingConsentCookieName))
                {
                    args.Session.CustomData[Settings.TrackingConsentCookieName] = consentCookie.Value;
                }
                else
                {
                    args.Session.CustomData.Add(Settings.TrackingConsentCookieName, consentCookie.Value);
                }
            }
        }
    }
}

Wir haben dann nur noch einen zentralen TrackingService implementiert, welcher jederzeit über den aktuellen Zustand der Zustimmung (1. Cookie, Fallback Session) Auskunft gibt.

Wechsel des Zustandes

Bei nachträglicher Änderung des Cookie Consents ist immer darauf zu achten, insbesondere aber bei Entzug der Zustimmung, dass neben der eigentlichen Änderung des Zustandes die Session auch zurückgesetzt wird, damit auf keinen Fall ungewollt weiter getrackt wird.

Rule ‘IsConsentGiven’

Damit auch redaktionell der Zustand vor und nach dem Cookie Consent abgebildet werden kann, empfehle ich eine entsprechende Bedingung zur Verwendung in der RuleEngine. Dahinter verbirgt sich nichts anderes als eine sog. WhenCondition, die den Zustand (siehe oben) des Cookie Consents überprüft. 

RuleEngine WhenCondition
Personalisierungsregel für Redakteure

Individualentwicklung

Über alles und unabhängig von der gewählten Variante gilt, das Thema der DSGVO-Konformität bleibt dauerhaft im Projekt bestehen und kann nie wirklich abgehakt werden. Jede Implementierung im Projekt bedarf der Überprüfung und ggf. muss ein Vorher/Nachher-Verhalten berücksichtigt werden.

Lösungsansätze je Tracking-Variante

Tracking = None

Fangen wir mit der naheliegendsten Variante an, zu der man auch schon so einiges im Netz findet: Der Besucher wird gar nicht erst getrackt und somit werden auch keine Analytics Daten erfasst. Für die Umsetzung dieser Variante sind vier Schritte erforderlich, eine erste Übersicht gibt die zugehörige Konfigurationsdatei:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <!-- Step 3 -->
    <services>
      <register serviceType="Sitecore.ExperienceForms.Analytics.Tracking.IAnalyticsTrackerResolver, Sitecore.ExperienceForms.Analytics" implementationType="Comspace.Sitecore.Usercentrics.ExperienceForms.AnalyticsTrackerResolver, Comspace.Sitecore.Usercentrics" lifetime="Transient" />
    </services>

    <pipelines>
      <!-- Step 1 -->
      <startAnalytics>
        <processor
          type="Comspace.Sitecore.Usercentrics.Pipelines.StartAnalytics.DisableTracker, Comspace.Sitecore.Usercentrics"
          patch:before="processor[@type='Sitecore.Analytics.Pipelines.StartAnalytics.CheckPreconditions, Sitecore.Analytics']"  resolve="true"/>
      </startAnalytics>

          <!-- Step 4 -->
      <mvc.customizeRendering>
        <processor
          type="Comspace.Sitecore.Usercentrics.Pipelines.CustomizeRendering.PersonalizeBugfix, Comspace.Sitecore.Usercentrics"
          patch:instead="processor[@type='Sitecore.Mvc.Analytics.Pipelines.Response.CustomizeRendering.Personalize, Sitecore.Mvc.Analytics']" />
      </mvc.customizeRendering>
    </pipelines>


    <events>
      <!-- Step 2 -->
      <event name="media:request">
        <handler patch:before="*[@method='OnMediaRequest']"
                 type="Comspace.Sitecore.Usercentrics.EventHandler.CancelMediaRequestTracking, Comspace.Sitecore.Usercentrics"
                 method="OnMediaRequest" resolve="true"/>
      </event>
    </events>
  </sitecore>
</configuration>

Schritt 1: Tracking deaktivieren

Hier sind wir zunächst Dmytro Shevchenkos Ansatz gefolgt und haben gleich zu Beginn der startAnalytics-Pipeline den Tracker deaktiviert, sofern der Cookie Consent noch nicht erteilt wurde.

namespace Comspace.Sitecore.Usercentrics.Pipelines.StartAnalytics
{
    public class DisableTracker
    {
        //...
        
        public void Process(PipelineArgs args)
        {            
            if(!TrackingService.IsConsentGiven())
            {
                Tracker.Enabled = false;
            }
        }
    }
}

Schritt 2 Exception beim Media Request Tracking

Aber die Deaktivierung ist für den produktiven Einsatz noch nicht ausreichend, denn es taucht dann noch folgende Fehlermeldung im  Logfile auf:

Exception: System.ArgumentNullException
Message: Tracker.Current is not initialized.
Parametername: Tracker
Source: Sitecore.Framework.Conditions
   bei Sitecore.Framework.Conditions.RequiresValidator`1.ThrowExceptionCore(String condition, String additionalMessage, ConstraintViolationType type)
   bei Sitecore.Framework.Conditions.Throw.ValueShouldNotBeNull[T](ConditionValidator`1 validator, String conditionDescription)
   bei Sitecore.Framework.Conditions.ValidatorExtensions.IsNotNull[T](ConditionValidator`1 validator, String conditionDescription)
   bei Sitecore.Analytics.RobotDetection.Media.MediaRequestEventHandler.OnMediaRequest(Object sender, EventArgs args)

Im Sitecore.Analytics.Media.MediaRequestEventHandler (gleiches gilt für den Sitecore.Analytics.RobotDetection.Media.MediaRequestEventHandler) wird leider nicht  überprüft, ob der Tracker aktiviert wurde (Tracker.Enabled=true) oder nicht. Dies geschieht lediglich für die globale Konfiguration der Instanz bzw. der Site. 
Im Verlauf wird dann die startTracking-Pipeline aufgerufen, in welcher wir ja im ersten Schritt die Initialisierung des Trackers verhindert haben und direkt im Anschluss wird die Initialisierung des Trackers wie folgt abgesichert:

Assert.IsNotNull((object) Tracker.Current, "Tracker.Current is not initialized");

Als pragmatische Lösung erschien es mir in diesem Fall, das Media Tracking Event einfach abzubrechen (natürlich nur sofern die Zustimmung noch nicht erteilt wurde):

namespace Comspace.Sitecore.Usercentrics.EventHandler
{
    public class CancelMediaRequestTracking
    {
        //...
        
        public void OnMediaRequest(object sender, EventArgs args)
        {
            if(!TrackingService.IsConsentGiven())
            {
                ((SitecoreEventArgs) args).Result.Cancel = true;
            }
        }
    }
}

Schritt 3: Experience Forms

Irgendwann ist mir beim Testen aufgefallen, dass manchmal “plötzlich” doch der Analytics-Cookie angelegt wurde, aber ohne Tracking-Daten zu erzeugen? Ja, manchmal hatte ich einfach vergessen ein privates Fenster im Browser zu öffnen und dann hatte ich halt schon zugestimmt… Aber irgendwann bin ich der Ursache doch auf die Schliche gekommen. In Sitecore Forms ist das sog. Performance Tracking standardmäßig aktiviert:

Advanced Settings > Performance tracking enabled
Sitecore Forms – Performance Tracking

Kurz hatte ich darüber nachgedacht, dass Problem zu ignorieren, da es sich ja um ein redaktionell lösbares handelt. Den Haken kann man ja auch deaktivieren. Aber das ist ein Gedankenfehler, man möchte das Formular ja vielleicht nach der Zustimmung trotzdem tracken…

Zur Lösung des Problems: Leider verwendet Sitecore Forms nicht die Standard startAnalytics-Pipeline zur Initialisierung des Trackers. Hinzu kommt: Es gibt keinen “schönen” Erweiterungspunkt im genutzten “Sitecore.ExperienceForms.Analytics.Tracking.AnalyticsTrackerResolver”, um die Zustimmung vorab überprüfen zu können. Daher habe wir den bestehenden AnalyticsTrackerResolver kopiert und den geänderten Quellcode entsprechend gekennzeichnet. Die Kopie wäre dann natürlich bei einem Upgrade erneut zu überprüfen.

namespace Comspace.Sitecore.Usercentrics.ExperienceForms
{
    public class AnalyticsTrackerResolver : IAnalyticsTrackerResolver
    {
        // ...

        public ICurrentPageContext CurrentPage
        {
            get
            {
                //Begin Edit
                if (!TrackingService.IsConsentGiven())
                {
                    return null;
                }
                //End Edit
                if (!Tracker.Enabled)
                    return null;
                if (Tracker.Current == null)
                    StartTracking();
                return Tracker.Current?.Session?.Interaction?.CurrentPage;
            }
        }
    }
}

Die Erstellung eines Cookies ohne Zustimmung ist die eine Sache, aber sofern der Besucher später im Verlauf der Session noch seine Zustimmung erteilen würde, dann würde auch die Formulareingabe von vor der Zustimmung nachträglich noch persistiert werden…

Schritt 4: Is it a bug or a feature?

Zu guter Letzt möchte ich euch noch auf folgendes Verhalten in Sitecore hinweisen: Solange der Tracker nicht gestartet wurde (was wir ja oben bewusst verhindern), funktioniert KEINE Personalisierung mehr, also z.B. auch keine einfache Datums-Regel.
Die Ursache und Lösung sind in diesem Blog-Artikel (Thanks to Bas Lijten) genau beschrieben. Ich würde es als Bug einstufen, habe es mit dem Support aber nicht ausdiskutiert. 😉

Die Lösung in Kürze: In der “mvc.customizeRendering”-Pipeline ist der Personalize-Processor zu ersetzen:

namespace Comspace.Sitecore.Usercentrics.Pipelines.CustomizeRendering
{
    /// <summary>
    /// See https://blog.baslijten.com/sitecore-analytics-cookie-consent-and-personalization-isnt-a-great-match-learn-how-to-keep-sitecore-functional-without-breaking-the-law/
    /// </summary>
    public class PersonalizeBugfix : Personalize
    {
        public override void Process(CustomizeRenderingArgs args)
        {
            if (args.IsCustomized) //|| !Tracker.IsActive) 
                return;

            Evaluate(args);
        }
    }
}

Tracking = InSession

Eine weitere mögliche Variante des Trackings vor der Zustimmung ist ein rein Session-basiertes Tracking, ohne Persistierung der Daten in der Analytics Datenbank (xDB).

Für die Umsetzung dieser Variante sind zwei Schritte erforderlich, eine erste Übersicht gibt die zugehörige Konfigurationsdatei:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <!-- Step 1 -->
      <createTracker>
        <processor
          type="Comspace.Sitecore.Usercentrics.Pipelines.CreateTracker.RemoveAnalyticsCookie, Comspace.Sitecore.Usercentrics"
          patch:after="processor[@type='Sitecore.Analytics.Pipelines.CreateTracker.GetTracker, Sitecore.Analytics']" resolve="true"/>
      </createTracker>

      <!-- Step 2 -->
      <commitSession>
        <processor
          type="Comspace.Sitecore.Usercentrics.Pipelines.CommitSession.SkipSesssionCommit, Comspace.Sitecore.Usercentrics"
          patch:before="processor[@type='Sitecore.Analytics.Pipelines.CommitSession.CheckPreconditions, Sitecore.Analytics']" resolve="true"/>
      </commitSession>
    </pipelines>
  </sitecore>
</configuration>

Schritt 1: Analytics Cookie unterbinden

Sobald Sitecore den Tracker startet, wird auch der Analytics-Cookie angelegt, obwohl Sitecore diesen nicht weiter benötigt, denn der Tracker ist ASP.NET-Session-basiert. Die Anlage des Cookies würde aber den Besucher zu der falschen Annahme verleiten, dass er getrackt wird. Daher sollte dieser auch nicht gesetzt werden. 
Das Setzen des Cookies ist leider in dem getTracker-Processor so tief verankert, dass es eine pragmatische Lösung sein kann, den Analytics-Cookie direkt im anschließenden Processor sofort wieder zu invalidieren:

namespace Comspace.Sitecore.Usercentrics.Pipelines.CreateTracker
{
    public class RemoveAnalyticsCookie: CreateTrackerProcessor
    {
        //...

        public override void Process(CreateTrackerArgs args)
        {
            if (!TrackingService.IsConsentGiven(args.Tracker.Session))
            {
                if (HttpContext.Current.Request.Cookies[AnalyticsCookie] != null)
                {
                    var myCookie = new HttpCookie(AnalyticsCookie)
                    {
                        Expires = DateTime.Now.AddDays(-1d)
                    };
                    HttpContext.Current.Response.Cookies.Add(myCookie);
                }
            }
        }
    }
}

Schritt 2: Persistierung der Tracking-Daten verhindern 

Als Letztes muss nur noch die Persistierung der Tracking-Daten verhindert werden. Das ist erstmal relativ einfach, es muss gleich zu Beginn der commitSession-Pipeline die Pipeline abgebrochen werden, solange die Zustimmung noch nicht erfolgt ist:

namespace Comspace.Sitecore.Usercentrics.Pipelines.CommitSession
{
    public class SkipSesssionCommit
    {
        //...
        
        public void Process(PipelineArgs args)
        {   
            if (!TrackingService.IsConsentGiven())
            {
                args.AbortPipeline();
            }
        }
    }
}

Hinweis: Diese Pipeline wird automatisch nach Ablauf der Session ausgeführt, der Cookie mit dem Status der Zustimmung steht in diesem Fall also nicht zur Verfügung (siehe Absatz zur Gemeinsamkeit).

Tracking = Anonym

Das ist die einfachste Variante, da für die Implementierung keine zusätzliche Individualentwicklung erforderlich ist. Besonderen Augenmerk solltet ihr in diesem Fall aber auf die Konfiguration des IP-Hashing legen.

Abschluss

Die Grundsatzfrage bleibt bestehen: Was ist im Kundenprojekt gewünscht und erlaubt? 

Aber zumindest hinsichtlich der technischen Lösungen, hoffe ich dass für euch was dabei war. Habt ihr noch ganz andere Varianten, Ansätze oder Stolpersteine? Dann freue ich mich auf euer Feedback.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Dieses Formular speichert Deinen Namen, Deine E-Mail-Adresse sowie den Inhalt, damit wir die Kommentare auf unserer Seite auswerten und anzeigen können. Weitere Informationen findest Du in unserer Datenschutzerklärung.