Edge Consent Form

Arto Kylmanen

Sept. 14, 2023


Introduction

Privacy regulations aroung online tracking have been on the table for years and now more than ever. While the enforcement of tracking compliance has been lacking in many jurisdictions, it has been ramping up in recent years and is not expected to change anytime soon.

There are many ways to implement consent forms. We have plugins, libraries, SaaS tools and so on. However, I've always found some issues with many of these - they are just overkill. Each plugin adds complexity. From programmatic perspective consent management is deadly simple:

  1. Ask user consent for tracking
  2. Add tracking when/if consent is granted

This article shows an easy consent management method using a Cloudflare Worker. If your site is using Cloudflare already, implementation is simple and fast.

Outcome

As a result, we will have successfully implemented a consent form management from within Cloudflare's Edge Service. This advancement gives us a dynamic and responsive capability to deliver consent forms that align with the jurisdiction of individual users. This is particularly advantageous for static websites, such as the website you are on now. In the other hand, it can be beneficial for dynamic websites as well as it removes the need to write intricate geoparsing logic server-side.

For this article, I just focus on creating the barebone setup for the form. Further on, I'll expand upon this feature to create a more dynamic and functional form.

Important information!

Prerequisites

  1. Knowing JavaScript is helpful, but not required.
  2. DNS on CloudFlare with proxied traffic enabled. They offer solid documentation on how to do that.
  3. Design for the consent form
  4. GTM Container code snippets

If you are not familiar with JavaScript and Cloudflare Worker logic, I recommend that you read through the comments in code (Lines starting with double slash "//") to understand the general flow.

Create a new CF Worker instance

Login to Cloudflare and navigate to "Workers" section, which you can find on the left hand side

Image

Click on "Create a Service" to create new worker

Image

Select a name for your worker, you may leave other settings to default. Click "Create Service" to create new instance.

Image

Add boilerplate code to the worker

Once you have created a new worker, open it and click on "Quick Edit" on the top

right to open the Web IDE.

When you have the IDE open, copy-paste the below code to it.

Click "Save and Deploy"

Image
Worker Boilerplate code
// Wait for user request to be intercepted
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // Fetch the original page
  const response = await fetch(request)
  const body = await response.text()

  // Define the JavaScript code to be injected
  const script = `
    <script>
    console.log("Yey! The Cloudflare Worker is functional. Your country is: ${country}")
    </script>
  `

  // Inject the script into the intercepted HTML
  const modified = body.replace('</body>', script + '</body>')

  // Return the modified response
  return new Response(modified, {
    headers: response.headers
  })
}

Connect the worker to your website

Now that the worker boilerplate is deployed, we want to connect it to our domain.

With the worker opened, navigate to "Triggers" under which you will find a section "Routes". Add a new route.

Image 1

Define your route in format *yourdomain.com/*

You may want to also use staging URL in this step. In that case use *staging.yourdomain.com/*

Select a zone (your domain) from the dropdown. Then just add the route.

Test your setup

Before proceeding further, let's make sure that the worker is successfully intercepting and modifying the servers HTML response.

All you need to do is to navigate to any page of your website, open developer tools and look for console messages.

If you see the message in the console output, congratulations! You have successfully implemented an Edge Worker on your site.

Build the popup

Now that the Worker is successfully implented on site, it's time to build the popup and relevant JavaScript logic to monitor when and which button user clicks.

Update the handleRequest functions as follows:

Adding the popup
async function handleRequest(request) {
  // Fetch the original page
  const response = await fetch(request)
  const body = await response.text()

  // Define the JavaScript code to be injected
  const script = `
  <script>
    function showConsentPopup() {
    // Create the overlay
    var overlay = document.createElement('div');
    overlay.style.position = 'fixed';
    overlay.style.top = 0;
    overlay.style.left = 0;
    overlay.style.width = '100%';
    overlay.style.height = '100%';
    overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
    overlay.style.display = 'flex';
    overlay.style.justifyContent = 'center';
    overlay.style.alignItems = 'center';
    overlay.style.zIndex = 9999;

    // Create the popup
    var popup = document.createElement('div');
    popup.className = 'container bg-dark text-white';
    popup.style.width = '300px';
    popup.style.padding = '20px';
    popup.innerHTML = \`
    <div style=>
      <h2 class="text-center" style="margin-top:1em;">Privacy Consent</h2>
      <p class="text-center">This site uses Google Analytics to enhance your experience. Please accept or deny the use of cookies.</p>
      <br><p> No identifying data is collected. </p><br>
      <p>Your data is not shared with any 3rd parties</p>
      <p class="text-center">Your current location is: ${country}</p> 
      <div class="d-flex justify-content-between">
        <button id="accept" class="btn btn-primary">Accept</button>
        <button id="deny" class="btn btn-danger">Deny</button>
      </div>
      </div>
    \`;

    // Compile everything
    overlay.appendChild(popup);

    // Inject it to HTML, aka. show the popup
    document.body.appendChild(overlay);
    

    // Event listeners to look for the button clicks
    document.getElementById('accept').addEventListener('click', function() {
      document.body.removeChild(overlay);
    });

    document.getElementById('deny').addEventListener('click', function() {
      document.body.removeChild(overlay);
    });
  };

 // Wait for the page to load before showing the popup
  window.onload = function() {
    showConsentPopup();
  };

  </script>
  `

  // Inject the script into the intercepted HTML
  const modified = body.replace('</body>', script + '</body>')

  // Return the modified response
  return new Response(modified, {
    headers: response.headers
  })
}

You may apply your own HTML and design for the popup.innerHTML that matches your design.

When you're done, press "Save and Deploy" and go back to your page and reload. You should now see a popup that asks for consent and closes when you press "Accept" or "Deny"

Image

Building rest of the logic

You are almost complete! Now there are just two final things to do:

  1. Function that will inject GTM when consent is given
  2. Cookie that retains memory on users selection

To do this, we will first add a function that injects the tracking code and make it fire when "Accept" is hit. It will look like this:

GTM Tracking Injection
function tracking_insertion() {
    var gtmScript1 = document.createElement('script');
    gtmScript1.innerHTML = \`
        (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
        new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
        j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
        'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
        })(window,document,'script','dataLayer','GTM-XXXXX');
    \`;
    document.head.appendChild(gtmScript1);

    var gtmScript2 = document.createElement('noscript');
    gtmScript2.innerHTML = \`
        <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXX"
        height="0" width="0" style="display:none;visibility:hidden"></iframe>
    \`;
    document.body.appendChild(gtmScript2);
    }

Remeber to update the GTM container ID with yours.

Next, let's update the click event listeners. I have set cookie expiry to 12 months for Acceptance and 1 week to denial. You can tweak these to your preference.

Updated event listeners
document.getElementById('accept').addEventListener('click', function() {
      tracking_insertion();
      document.body.removeChild(overlay);
      var expiryDate = new Date();
      expiryDate.setTime(expiryDate.getTime() + (12*30*24*60*60*1000)); // 12 months in milliseconds
      var expires = "expires="+ expiryDate.toUTCString();
      document.cookie = "consentAccepted=true; " + expires + "; path=/";
    });

    document.getElementById('deny').addEventListener('click', function() {
      document.body.removeChild(overlay);
      var expiryDate = new Date();
      expiryDate.setTime(expiryDate.getTime() + (7*24*60*60*1000)); // 7 days in milliseconds
      var expires = "expires="+ expiryDate.toUTCString();
      document.cookie = "consentAccepted=false; " + expires + "; path=/";
    });

Finally, we want to update our function that fires when window has loaded with these three conditions:

  1. If the cookie is set to "true" inject the tracking script
  2. If the cookie is set to "false" terminate the script
  3. If there is no cookie, ask for consent

We can do this by updating the function that fires on window.onload() as so:

Check cookie status and act accordingly
window.onload = function() {
  // Get the consentAccepted cookie value
  var consentAcceptedCookie = document.cookie.split(';').find((item) => item.trim().startsWith('consentAccepted='));
  
  // Check the value of the consentAccepted cookie
  if (consentAcceptedCookie) {
    var consentAccepted = consentAcceptedCookie.split('=')[1];

    if (consentAccepted === 'true') {
      tracking_insertion();
    }
  } else {
    showConsentPopup();
  }

And we are done! Let's take a look at the full code once again

Code so far
// Wait for user request to be intercepted
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

async function handleRequest(request) {
  // Fetch the original page
  const response = await fetch(request)
  const body = await response.text()

  // Define the JavaScript code to be injected
  const script = `
  <script>
    function tracking_insertion() {
      var gtmScript1 = document.createElement('script');
      gtmScript1.innerHTML = \`
          (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
          new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
          j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
          'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
          })(window,document,'script','dataLayer','GTM-XXXX');
      \`;
      document.head.appendChild(gtmScript1);

      var gtmScript2 = document.createElement('noscript');
      gtmScript2.innerHTML = \`
          <iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXX"
          height="0" width="0" style="display:none;visibility:hidden"></iframe>
      \`;
      document.body.appendChild(gtmScript2);
    }

    function showConsentPopup() {
      if (document.cookie.split(';').some((item) => item.trim().startsWith('consentAccepted='))) {
        return;
    }
    // Create the overlay
    var overlay = document.createElement('div');
    overlay.style.position = 'fixed';
    overlay.style.top = 0;
    overlay.style.left = 0;
    overlay.style.width = '100%';
    overlay.style.height = '100%';
    overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
    overlay.style.display = 'flex';
    overlay.style.justifyContent = 'center';
    overlay.style.alignItems = 'center';
    overlay.style.zIndex = 9999;

    // Create the popup
    var popup = document.createElement('div');
    popup.className = 'container bg-dark text-white';
    popup.style.width = '300px';
    popup.style.padding = '20px';
    popup.innerHTML = \`
    <div style=>
      <h2 class="text-center" style="margin-top:1em;">Privacy Consent</h2>
      <p class="text-center">This site uses Google Analytics to enhance your experience. Please accept or deny the use of cookies.</p>
      <br><p> No identifying data is collected. </p><br>
      <p>Your data is not shared with any 3rd parties</p>
      <p class="text-center">Your current location is: ${country}</p> 
      <div class="d-flex justify-content-between">
        <button id="accept" class="btn btn-primary">Accept</button>
        <button id="deny" class="btn btn-danger">Deny</button>
      </div>
      </div>
    \`;

    // Compile everything
    overlay.appendChild(popup);

    // Inject it to HTML, aka. show the popup
    document.body.appendChild(overlay);
    

    // Event listeners to look for the button clicks
    document.getElementById('accept').addEventListener('click', function() {
      tracking_insertion();
      document.body.removeChild(overlay);
      var expiryDate = new Date();
      expiryDate.setTime(expiryDate.getTime() + (30*24*60*60*1000)); // 30 days in milliseconds
      var expires = "expires="+ expiryDate.toUTCString();
      document.cookie = "consentAccepted=true; " + expires + "; path=/";
    });

    document.getElementById('deny').addEventListener('click', function() {
      document.body.removeChild(overlay);
      var expiryDate = new Date();
      expiryDate.setTime(expiryDate.getTime() + (1*24*60*60*1000)); // 1 day in milliseconds
      var expires = "expires="+ expiryDate.toUTCString();
      document.cookie = "consentAccepted=false; " + expires + "; path=/";
    });
  };

  window.onload = function() {
    showConsentPopup();
  };

  </script>
  `

  // Inject the script into the intercepted HTML
  const modified = body.replace('</body>', script + '</body>')

  // Return the modified response
  return new Response(modified, {
    headers: response.headers
  })
}

Debug Again

We are complete. Let's now do a final test. Navigate to your page and open devtools. Navigate to "Application" > "Cookies" and click on your domain. You should see cookie for consentAccepted with a value true or false, depending on what you selected.

Good debug process is crucial for us to make sure that everything is working as expected. I recommend checking things like so:

  1. Consent popup is showing up
  2. Cookie is saved with the correct value
  3. Reload the page: If concent is given previously, GTM is injected
  4. Reload the page: If consent is denied, nothing happens.

If you are operating strictly on one market, give yourself a high five and call it a day!

Image

Back to index

Copied to clipboard