Making Sitecore XP Compliant with GDPR and Other Privacy Laws (Because It's Not)

As of April 2023, Sitecore XP (up to and including 10.3) is not GDPR-compliant out of the box (depending on which lawyers you ask). Given the steep fines of privacy compliance, this is probably not something you want to risk to a gray area technicality.

In Sitecore 10.0, the ConsentManager API was introduced to allow developers to more easily manage a user’s consent and disable/enable tracking accordingly. According to Sitecore’s documentation, the GiveConsent() method writes the consent choice to the SC_TRACKING_CONSENT cookie and initializes the tracker for the user. Easy enough. The RevokeConsent() method stops any tracking that’s already in progress, invalidates the SC_ANALYTICS_GLOBAL_COOKIE, and updates the SC_TRACKING_CONSENT cookie to reflect the consent choice. Okay, great. That’s exactly what we want.

The Problem

Unfortunately, Sitecore’s own code doesn’t (always) use this API. Whenever Sitecore does something that requires a Tracker, it tries to create an instance of DefaultTracker and initializes it, including running pipelines such as ensureSessionContext. In the ensureSessionContext pipeline, there is a processor Sitecore.Analytics.Pipelines.EnsureSessionContext.EnsureDevice that executes the following code:

ContactKeyCookie contactKeyCookie = new ContactKeyCookie();
...
contactKeyCookie.Create(args.Session.Device.DeviceId);

That creates the SC_ANALYTICS_GLOBAL_COOKIE to store the device ID. That’s okay, because there’s another processor Sitecore.Analytics.Pipelines.EnsureSessionContext.CheckConsent that executes afterwards that invalidates this based on a user’s consent choice. Except, it doesn’t. Here’s the code, see if you can figure out which Sitecore-provided consent API you don’t see used here:

bool? consent = args.Session.Contact.IsTrackingConsentGivenFor(Context.Site.Name);
if (!consent.HasValue)
	this._consentStorage.RemoveConsent(HttpContext.Current.ToHttpContextBase());
else
	this._consentStorage.SetConsent(HttpContext.Current.ToHttpContextBase(), new TrackingConsent()
	{
		IsGiven = false
	});
if (!args.Session.IsReadOnly)
	this._sharedSessionStateManager.ReleaseContact(args.ContactId.Value);
args.Session.Contact = null;
public void RemoveConsent(HttpContextBase httpContext)
{
	var cookie = httpContext.GetCookie("SC_TRACKING_CONSENT");

	if (cookie == null)
		return;

	var siteConsentList = new List<CookiesConsentStorage.SiteConsent>();

	if (!string.IsNullOrEmpty(cookie.Value))
		siteConsentList = this.GetSiteConsentsFromCookie(cookie);
	
	var siteName = this._currentSiteContext.SiteName;
	var siteConsent = siteConsentList.FirstOrDefault<CookiesConsentStorage.SiteConsent>((Func<CookiesConsentStorage.SiteConsent, bool>) (s => s.SiteName == siteName));
	
	if (siteConsent != null)
		siteConsentList.Remove(siteConsent);
	
	if (siteConsentList.Any<CookiesConsentStorage.SiteConsent>())
	{
		cookie.Value = this.ConvertSiteConsentsToCookieValue(siteConsentList);
	}
	else
	{
		cookie.Value = string.Empty;
		cookie.Expires = DateTime.UtcNow.AddDays(-1.0);
	}

	httpContext.Response.Cookies["SC_TRACKING_CONSENT"].Value = cookie.Value;
	httpContext.Response.Cookies["SC_TRACKING_CONSENT"].Expires = cookie.Expires;
}

Instead of using the API, the code only updates the SC_TRACKING_CONSENT cookie but doesn’t invalidate the all-important SC_ANALYTICS_GLOBAL_COOKIE which gets created when the tracker gets created. This means the SC_ANALYTICS_GLOBAL_COOKIE gets issued regardless of a user’s consent choice, which is Not Good™. While the cookie doesn’t contain anything more than a user’s device ID, according to the EU ePrivacy Directive (and in turn, GDPR), this is considered personally identifiable information that a user actively rejected you from collecting, and since all cookies are sent with every request, the value of that cookie gets sent to the server and the legal argument of whether that data is “processed” or not begins.

The Solution

What we can do is to rewrite the CheckConsent processor to use the ConsentManager API and have it properly revoke the cookie.

Here’s the new and improved rewritten CheckConsent processor:

using Sitecore;
using Sitecore.Abstractions;
using Sitecore.Analytics.Configuration;
using Sitecore.Analytics.Pipelines.InitializeTracker;
using Sitecore.Diagnostics;
using Sitecore.Analytics.Tracking.Consent;

namespace Foundation.Personalization.Pipelines.EnsureSessionContext
{
    public class CheckConsent : InitializeTrackerProcessor
    {
        public ICurrentSiteContext CurrentSiteContext { get; }
        public IConsentManager ConsentManager { get; }
        public BaseLog Log { get; }

        public CheckConsent(ICurrentSiteContext currentSiteContext, IConsentManager consentManager, BaseLog baseLog)
        {
            CurrentSiteContext = currentSiteContext;
            ConsentManager = consentManager;
            Log = baseLog;
        }

        public override void Process(InitializeTrackerArgs args)
        {
            Assert.ArgumentNotNull(args, nameof(args));

            if (Context.Site == null || !CurrentSiteContext.ExplicitConsentForTrackingIsRequired)
            {
                Log.Debug("CheckConsent is skipped for site: " + (Context.Site?.Name ?? "<undefined>"));
            }
            else
            {
                var consentChoice = ConsentManager.GetConsent(null);

                if (consentChoice == null || !consentChoice.IsGiven)
                {
                    ConsentManager.RevokeConsent(null);

                    args.AbortPipeline();
                    Log.Debug("CheckConsent aborts pipeline");
                }
            }
        }
    }
}

and the patch file to replace the default Sitecore one with this one:

<?xml version="1.0"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
		<ensureSessionContext>
			<processor type="Foundation.Compliance.Pipelines.EnsureSessionContext.CheckConsent, Foundation.Compliance" resolve="true" patch:instead="processor[@type='Sitecore.Analytics.Pipelines.EnsureSessionContext.CheckConsent, Sitecore.Analytics']" />
		</ensureSessionContext>
		</pipelines>
  </sitecore>
</configuration>

With this, for any requests that try to create a new tracker instance will have the SC_ANALYTICS_GLOBAL_COOKIE cookie invalidated before sending the response back to the user rather than merely resetting the consent choice.

Sitecore is aware of this bug, however, they have no timeline or plans to resolve. If you want to follow along, Sitecore support has given the reference number 526015.

EDIT: I was mistaken, Sitecore is not aware of the bug. Intead, 526015 is for another bug where Sitecore Forms doesn’t respect the consent choice and enables the cookie for form field tracking. Big yikes.

** This blog post should not be taken as any sort of legal guidance. You should consult with a law professional for your specific use-case. It’s also code you found on the internet, so you’re on your own as far as support goes.