This is the next iteration of the original cookie consent guide I wrote. The new version works the following way:

  • Cookies are divided into one of four categories using a free consent management platform: (1) Strictly necessary cookies, (2) functional cookies, (3) analytics/performance cookies, and (4) marketing cookies
  • The user lands on your site and the only tag that initially fires is the cookie banner, allowing the user to set their preferences (but without hindering access)
  • Based on the user’s preferences, only the tags will fire that belong to the category of tags they’ve accepted
    • The benefit here is that while the default will be to accept all cookies, it gives users the option to disallow some cookies (e.g. marketing) while still allowing others (such as analytics)

Disclaimer: I am not a lawyer and this guide is not meant as legal advice. Instead, consider it a best practice recommendation that lets your cookie consent form be compliant without overdoing it. There are many real-life examples of companies that are so over-compliant, they’re undoubtedly losing valuable analytics insights even though there is no legal need.

The aspects I was uncertain about I cross-checked with GDPR lawyers and they confirmed compliance. This guide is based on an article by Julius Fedorovicius, which I’ve adapted to increase clarity and ease of implementation.

5 Steps to GDPR-compliant cookie consent

0. Pre-requisites:

  • Google Analytics Account
  • Google Tag Manager Account

1. Request a free consent management platform account

  • Go to https://www.onetrust.com/free-edition/ and request a free account
  • The request typically takes two working days. Alternatively, a paid account costs $30/month (or use the code swdc19 and save 10%)
  • After you receive access to your OneTrust account and set it up, you’ll create a custom HTML tag with the script from your OneTrust account (setup details below)

2. Set up your OneTrust Account

2.1 Nav Menu → Websites → Add a website:

  1. Enter just the domain and without https or www (e.g. sebastianwismayer.com), this ensures it will work across subdomains as well. 
  2. Target pages to scan: From experience, it currently seems better to enter the ULRs of all pages in order for the tool to identify as many cookies as possible. Use a tool like Screaming Frog to determine all pages
  3. Wait for the scan to finish (unless your site is massive this should take less than half an hour)

2.2 Nav Menu → Cookie Banner :

  1. Layout + Colors can be adjusted how you wish
  2. Content:
    • Notice Description (unless you have a different, preferred one): “By continuing to browse or by clicking ‘Accept Cookies’, you agree to the storing of first- and third-party cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts.”
    • Cookie Settings Button: Yes (“Cookie Settings”)
    • Accept Cookies Button: Yes  (“Accept Cookies”)
  3. Behavior:
    • Toggle to yes the following: 
      • Close button accepts all cookies
      • Click accepts all cookies and closes banner
      • Scroll accepts all cookies and closes banner

2.3 Nav Menu → Cookie List:

  1. Assing cookies:
    • Update from scan
    • Auto Assign
      • For the cookies that the tool is unable to assign automatically, look up the cookie in Google and determine which category of cookies it falls into
      • IMPORTANT: Make sure you have at least one cookie in each of the four categories (Strictly Necessary, Performance, Functional, and Targeting). If you don’t, the category doesn’t appear in the cookie preference center and cannot be switched to ‘active’, resulting in all cookies of that category not firing
  2. Consent Settings:
    • Set the Consent Model to “Implied Consent”
      • Strictly Necessary Cookies are “Always Active”
      • The other three should be set to “Inactive LandingPage”

2.4 Nav Menu → Script Integration:

  1. Here you can change the overall look of the Cookie Consent Preference Center, which includes text, colors, and logo. Match it to your CI.

2.5 Nav Menu → Preference Center:

  1. Copy the Production CDN code and create the Tag: cHTML – Cookie Consent in your Google Tag Manager account (explained in more detail in step 5 below)
  2. Cookie Settings Button: Place the snippet on your site (e.g. in the footer) to allow users to adjust their cookie settings. 
  3. Publish

3. Add the necessary variables to Google Tag Manager

We will create 7 necessary variables (1 cookie, 3 JavaScript codes, 3 data layer variables)

1. Cookie for Consent

  • Variable Type: 1st Party Cookie
  • Name: Cookie – Actual Cookie Consent
  • Cookie Name: actualOptanonConsent

2. Custom JavaScript – Functional Cookies

  • Variable Type: Custom JavaScript
  • Name: Custom JS – Functional Cookies Allowed
function() {
  var consentFromCookie = {{Cookie - Actual Cookie Consent}};
  var consentFromDataLayer = {{dlv - Active Consent Groups}};
  if (consentFromDataLayer != undefined && consentFromCookie == undefined) {
    if (consentFromDataLayer.indexOf(",3,") >= 0) {
      return true // functional cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie != undefined) {
    if (consentFromCookie.indexOf(",3,") >= 0) {
      return true // functional cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer != undefined && consentFromCookie != undefined) {
    if (consentFromDataLayer.indexOf(",3,") >= 0) {
      return true // functional cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie == undefined) {
    return false // functional cookies not allowed
  }
}

3. Custom JavaScript – Performance and Analytics Cookies

  • Variable Type: Custom JavaScript
  • Name: Custom JS – Performance and Analytics Tracking Allowed
function() {
  var consentFromCookie = {{Cookie - Actual Cookie Consent}};
  var consentFromDataLayer = {{dlv - Active Consent Groups}};
  if (consentFromDataLayer != undefined && consentFromCookie == undefined) {
    if (consentFromDataLayer.indexOf(",2,") >= 0) {
      return true // analytics allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie != undefined) {
    if (consentFromCookie.indexOf(",2,") >= 0) {
      return true // analytics allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer != undefined && consentFromCookie != undefined) {
    if (consentFromDataLayer.indexOf(",2,") >= 0) {
      return true // analytics allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie == undefined) {
    return false // analytics not allowed
  }
}

4. Custom JavaScript – Marketing Cookies

  • Variable Type: Custom JavaScript
  • Name: Custom JS – Marketing Cookies Allowed
function() {
  var consentFromCookie = {{Cookie - Actual Cookie Consent}};
  var consentFromDataLayer = {{dlv - Active Consent Groups}};
  if (consentFromDataLayer != undefined && consentFromCookie == undefined) {
    if (consentFromDataLayer.indexOf(",4,") >= 0) {
      return true // marketing cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie != undefined) {
    if (consentFromCookie.indexOf(",4,") >= 0) {
      return true // marketing cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer != undefined && consentFromCookie != undefined) {
    if (consentFromDataLayer.indexOf(",4,") >= 0) {
      return true // marketing cookies allowed
    } else {
      return false
    }
  }
  if (consentFromDataLayer == undefined && consentFromCookie == undefined) {
    return false // marketing cookies not allowed
  }
}

5. Data Layer Variable – Active Consent

  • Variable Type: Data Layer Variable
  • Name: dlv – Active Consent Groups
  • Data Layer Variable Name: OnetrustActiveGroups
  • Data Layer Version: Version 2

6. Data Layer Variable – Engaging with the cookie consent banner

  • Variable Type: Data Layer Variable
  • Name: dlv – optanonAction
  • Data Layer Variable Name: optanonAction
  • Data Layer Version: Version 2

7. Data Layer Variable – Keeping track of consent

  • Variable Type: Data Layer Variable
  • Name: dlv – consentData for the record
  • Data Layer Variable Name: consentData
  • Data Layer Version: Version 2

Bonus. Google Analytics

While not strictly necessary a part of this guide, I highly recommend setting up your Google Analytics Settings as a variable within Google Tag Manager. It allows you to set anonymized IP addresses, which is a further requirement for compliance.

  • Variable Type: Google Analytics Settings
  • Name: Google Analytics
  • Tracking ID: enter your tracking ID here
  • Cookie Domain: auto
  • More settings -> Fields to Set
    • Field Name: anonymizeIp
    • Value: true

4. Add the necessary triggers to Google Tag Manager

We will create 10 necessary triggers (3 Pageview triggers, 4 blocking triggers, and 3 custom triggers)

1. Pageview – Functional Cookies

  • Trigger Type: Page view
  • Name: Pageview – All Pages – Functional Cookies Allowed
  • Trigger fires on: Some Page Views
    • Custom JS – Functional Cookies Allowed equals true

2. Pageview – Performance and Analytics Cookies

  • Trigger Type: Page view
  • Name: Pageview – All Pages – Analytics Tracking Allowed
  • Trigger fires on: Some Page Views
    • Custom JS – Performance and Analytics Tracking Allowed equals true

3. Pageview – Marketing Cookies

  • Trigger Type: Page view
  • Name: Pageview – All Pages – Marketing Cookies Allowed
  • Trigger fires on: Some Page Views
    • Custom JS – Marketing Cookies Allowed equals true

4. Blocking – Functional Cookies

  • Trigger Type: Custom Event
  • Name: Blocking – Functional Cookies are Not Allowed
  • Event Name: .*
  • Trigger fires on: Some Custom Events
    • Custom JS – Functional Cookies Allowed equals false

5. Blocking – Analytics Cookies

  • Trigger Type: Custom Event
  • Name: Blocking – Analytics Tracking is Not Allowed
  • Event Name: .*
  • Trigger fires on: Some Custom Events
    • Custom JS – Performance and Analytics Tracking Allowed equals false

6. Blocking – Marketing Cookies

  • Trigger Type: Custom Event
  • Name: Blocking – Marketing Cookies are Not Allowed
  • Event Name: .*
  • Trigger fires on: Some Custom Events
    • Custom JS – Marketing Cookies Allowed equals false

7. Blocking – Total Optout

  • Trigger Type: Custom Event
  • Name: Blocking – Total Optout
  • Event Name: .*
  • Trigger fires on: Some Custom Events
    • Custom JS – Functional Cookies Allowed equals false
    • Custom JS – Performance and Analytics Tracking Allowed equals false
    • Custom JS – Marketing Cookies Allowed equals false

8. Consent Data for the Record

  • Trigger Type: Custom Event
  • Name: Custom – Consent Data For The Record
  • Event Name: consentDataForTheRecord
  • Trigger fires on: All Custom Events
    • Custom JS – Functional Cookies Allowed equals false
    • Custom JS – Performance and Analytics Tracking Allowed equals false
    • Custom JS – Marketing Cookies Allowed equals false

9. Cookie Notification Closed

  • Trigger Type: Custom Event
  • Name: Custom – Cookie Notification Closed
  • Event Name: trackOptanonEvent
  • Trigger fires on: Some Custom Events
    • dlv – optanonAction matches RegEx (ignore case) Banner Accept Cookies|Banner Auto Close|Banner Close Button|Preferences Save Settings

10. Consent Updated

  • Trigger Type: Custom Event
  • Name: Custom – Optanon Consent Updated
  • Event Name: optanonConsentUpdated
  • Trigger fires on: All Custom Events
    • dlv – optanonAction matches RegEx (ignore case) Banner Accept Cookies|Banner Auto Close|Banner Close Button|Preferences Save Settings

5. Add the 4 necessary tags to Google Tag Manager

1. Cookie Consent

  • Tag Type: Custom HTML
  • Name: cHTML – Cookie Consent
  • Firing Trigger: Pageview – DOM ready
    • This trigger fires on: All DOM ready events
  • HTML: Copy and paste the Production CDN code from your OneTrust account here (see step 2.5 above if you’re unsure where to find it)

2. Consent Data for the Record

  • Tag Type: Custom HTML
  • Name: cHTML – Push To Data Layer – Consent Data For The Record
  • Firing Trigger: Custom – Optanon Consent Updated
<script>
  var timestamp = new Date().toString();
  var userAgent = navigator.userAgent;

  function check_ga() {
    if ({{Custom JS - Performance and Analytics Tracking Allowed}} === true) {
      if (typeof ga === 'function' && typeof ga.getAll === 'function') {
        var clientId = ga.getAll()[0].get('clientId');
        window.dataLayer.push({
          'event': 'consentDataForTheRecord',
          'consentData': "Consent groups: " + {{Cookie - Actual Cookie Consent}} + "; " + timestamp + "; " + clientId + "; " + userAgent
        });
      } else {
        setTimeout(check_ga, 500);
      }
    } else {
      window.dataLayer.push({
        'event': 'consentDataForTheRecord',
        'consentData': "Consent groups: " + {{Cookie - Actual Cookie Consent}} + "; " + timestamp + "; clientId is unavailable; " + userAgent
      });
    }
  }
  check_ga();
</script>

3. Consent Updated

  • Tag Type: Custom HTML
  • Name: cHTML – Push To Data Layer – Consent Updated
  • Firing Trigger: Custom – Cookie Notification Close
<script>
  setTimeout(function(){ 
    window.dataLayer.push({
    'event': 'optanonConsentUpdated'
    }) 
  
  }, 1000);
</script>

4. Actual Cookie Consent Active Groups

  • Tag Type: Custom HTML
  • Name: cHTML – Set Cookie – Actual Cookie Consent Active Groups
  • Firing Trigger: Custom – Optanon Consent Updated
<script>
 
 var cookieName = "actualOptanonConsent"; 
 var cookieValue = encodeURIComponent({{dlv - Active Consent Groups}});
 var expirationTime = 31536000; // One year in seconds
 expirationTime = expirationTime * 1000; // Converts expirationtime to milliseconds
 var date = new Date(); 
 var dateTimeNow = date.getTime(); 

 date.setTime(dateTimeNow + expirationTime);
 var expirationTime = date.toUTCString();
 document.cookie = cookieName+"="+cookieValue+"; expires="+expirationTime+"; path=/; domain=." + location.hostname.replace(/^www\./i, ""); 

</script>

Adding your own tags

  1. When adding new tags to your GTM container, make sure to:
    • Add a firing trigger
    • And a blocking trigger based on the user’s preferences.
      • I.e.: The tag fires when consent is updated and marketing cookies are allowed, but is blocked when marketing cookies are not allowed. (Example for the Facebook conversion pixel below)
  • Similarly, if you’re setting up Google Analytics through Google Tag Manager (and you should), you should set up the tag to fire when Analytics Tracking is allowed and blocked when Analytics Tracking is Not Allowed (see below)

Summary

And there you have it!
Let’s recap how it works:

  • The user visits your site and the only tag that fires is the cookie banner, asking for consent
  • On accepting all (or some) cookies, the user’s consent is stored as a first party cookie
  • The record of their consent is also collected
  • If they disallowed any of the cookie groups, this choice becomes a blocking trigger for the corresponding tags
  • The cookie groups they allowed can fire normally
  • At any point, the user can update their cookie preferences

I hope you like the new and improved cookie consent, that is both GDPR-compliant and relatively simple to setup if you follow the step-by-step guide.

Let me know in the comments how it worked for you!