← Back to Technical SEO articles
Technical SEO

Is your cookie banner aligned with your tracking?

A case study of the Sagrada Família website, looking at cookies, localStorage, the dataLayer, Google Tag Manager, and what actually changes in the browser when a visitor accepts or rejects tracking.

Barcelona is one of the most popular cities to visit in 2026. Every year, millions of people experience the monuments, streets, food, culture, and architecture that Barcelona offers, including the famous church designed by Gaudí: La Sagrada Família.

I grew up near Barcelona, so I know the city pretty well. As a kid, I did not really understand how special Barcelona was until later, when I realized how many cultural activities, museums, outdoor places, beaches, and architectural sites we had around us.

As I became more exposed to marketing, analytics, and web performance, I started to see Barcelona differently. I started to notice how the city positioned itself as one of the most beautiful places in the world, with world-class food, cultural offers, and natural attractions. The beach and the mountains are both accessible in less than one hour.

Today, I want to explore how websites communicate with marketing tools such as Google Tag Manager, Google Analytics 4, and other platforms through the famous dataLayer.

We are going to look at how cookies work, how they are stored, and how data is shared from websites to marketing and analytics tools such as GTM and GA4.

Before starting, this is something useful to remember:

Cookies are the website’s memory. The dataLayer is the website’s messenger.

When a user makes a cookie choice, the cookie stores that choice for later. The dataLayer tells Google Tag Manager what changed right now.

This is where cookie banners and tracking overlap: the banner choice must be stored in the browser and also communicated correctly to the tracking tools.

This article is for three types of readers:

  • If you want to understand how cookies work behind the scenes, what changes in the browser when you make a choice, and how to debug that yourself.
  • If you have experienced website cookies hundreds of times but want to understand the technical side, including how cookies relate to the dataLayer.
  • If you have worked with dataLayer events but never really worked with cookies, or the other way around. Maybe you want the tools to understand how website events send analytics data to your marketing tools.

Note: this article is not meant for advanced developers who already have years of experience working with dataLayer implementations and cookie banners. Some parts may feel basic if you already work with this every day.

With that in mind, let’s start with a real-life scenario: the Sagrada Família website.

Part 1: What are cookies?

Cookies are small text files stored on your device by your web browser when you visit a website. They act as the website’s memory, allowing it to remember your actions, login status, and preferences, so you do not have to repeat them on every visit.

They can look something like this:

[
  {
    "name": "_ga",
    "value": "GA1.1.123456789",
    "domain": ".example.org",
    "expires": 1780000000
  }
]

When I opened the Sagrada Família website, before clicking anything, my inspection tool showed that there were no cookies being stored in my browser:

"cookies": [],
"documentCookie": "",
"localStorage": {
  "cookiefirst-id": "null"
}
Sagrada Familia Cookie Banner
Sagrada Familia Cookie Banner when opening the website.

But the moment I clicked “Deny" everything changed:

"storageAfterDecline": {
  "cookies": [
    {
      "name": "cookiefirst-consent",
      "domain": "sagradafamilia.org",
      "value": "%7B%22necessary%22%3Atrue%2C%22performance%22%3Afalse%2C%22functional%22%3Afalse%2C%22advertising%22%3Afalse%7D",
      "expires": 1813396915
    }
  ],
  "localStorage": {
    "cookiefirst-consent": "{\"necessary\":true,\"performance\":false,\"functional\":false,\"advertising\":false}",
    "cookiefirst-id": "\"7d40928a-ef52-4d50-9e98-39f2cc7f1dbd\""
  },
  "sessionStorage": {}
}

How do we interpret this result?

Field What it means
name cookiefirst-consent is the consent cookie created by CookieFirst. It stores the user’s cookie choice.
domain This means the cookie belongs to the Sagrada Família domain.
value This is the URL-encoded consent decision. When decoded, it becomes easier to read.
expires This is when the cookie expires. In this result, the expiration is around 360 days after the consent timestamp.
localStorage CookieFirst saved the same consent decision in localStorage. So the decision is stored both as a cookie and in localStorage.

When decoded, the consent value looks like this:

{
  necessary: true,
  performance: false,
  functional: false,
  advertising: false,
  timestamp: 1782292915,
  type: "category",
  version: "c279cd09-2ec6-45f9-8ba3-f9cce5e82b7b"
}

Local storage is different from cookies. It can store data in the browser, but it is not automatically sent with every server request in the same way cookies can be. That makes it useful for storing preferences or consent-related information in the browser.

What happens if I accept all cookies?

When I accepted all cookies, the browser storage changed completely.

Before making a choice, there were no cookies stored and documentCookie was empty. After accepting, the website created a cookiefirst-consent cookie where all categories were set to true: necessary (minimum standard for running a website), performance, functional, and advertising.

It also created Google-related cookies such as _gcl_au, _ga, and _ga_Z9S7Q33FL5.

In this case, accepting cookies allowed analytics and advertising tools to start storing identifiers in the browser.

It looked like this:

"cookies": [
  {
    "name": "cookiefirst-consent",
    "domain": "sagradafamilia.org",
    "value": "%7B%22necessary%22%3Atrue%2C%22performance%22%3Atrue%2C%22functional%22%3Atrue%2C%22advertising%22%3Atrue%7D"
  },
  {
    "name": "_gcl_au",
    "domain": ".sagradafamilia.org",
    "value": "1.1.1770990631.1782294059"
  },
  {
    "name": "_ga",
    "domain": ".sagradafamilia.org",
    "value": "GA1.1.696962384.1782294060"
  },
  {
    "name": "_ga_Z9S7Q33FL5",
    "domain": ".sagradafamilia.org",
    "value": "GS2.1.s1782294059$o1$g0$t1782294059$j60$l0$h1907544524"
  }
]

This is already useful. We can see that the cookie banner choice is not just a UI decision. It changes what is stored in the browser.

After decline, only the consent cookie and local storage values were saved, with optional categories set to false.

After accept, the consent cookie was updated and Google-related cookies appeared.

That is the kind of difference you want to verify when auditing a cookie banner.

Part 2: Understanding the dataLayer

The dataLayer is a JavaScript object that connects website events, such as clicks, scrolls, page loads, searches, purchases, or consent changes, to Google Tag Manager.

It helps create a more reliable connection between what happens on the website and what gets sent to analytics and marketing tools.

Google Analytics 4 gives you some standard events, such as page views and scrolls. But if you want to understand specific user behavior on your website, you usually need custom events.

With a dataLayer, you can send events such as login, purchase, search, video play, form submission, ticket selection, or checkout step. You can also pass extra values, such as selected date, product type, ticket category, language, clicked link destination, or product color.

For Sagrada Família, imagine there is a search bar for guided tours. The website could send an event such as search_term.

If many users search for “guided tours with video included” or “Sagrada Família with audio guide”, the business team might decide to create or promote that offer because the website is showing a demand signal.

Another useful case is tracking ancillaries.

What if the business could measure how many people buy the standard ticket and then add a guided tour with transport included? What if they could see how many people add a Barcelona T-shirt, an audio guide, or a tower visit?

These are useful business questions. A correct dataLayer setup helps website owners answer them.

In this case, the first check we can do is simple:

Does the website have a dataLayer when the page opens? And what events are already showing?

When I visited https://sagradafamilia.org/, I saw some interesting results.

Sagrada Familia Cookie Banner
Sagrada Familia Cookie Banner when opening the website.

First, there is a cookie banner. Before touching anything on that banner, I wanted to see what was happening behind the scenes.

I ran my code and got this:

{
  "exists": true,
  "isArray": true,
  "length": 4,
  "eventNames": ["gtm.js"],
  "lastEntries": [
    {
      "0": "consent",
      "1": "default",
      "2": {
        "ad_storage": "granted",
        "analytics_storage": "granted",
        "functionality_storage": "granted",
        "wait_for_update": 2000
      }
    },
    {
      "0": "consent",
      "1": "default",
      "2": {
        "ad_storage": "denied",
        "analytics_storage": "denied",
        "functionality_storage": "denied",
        "region": ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SI", "ES", "SE", "GB"],
        "wait_for_update": 2000
      }
    },
    {
      "0": "set",
      "1": {
        "developer_id.dNjAwYj": true
      }
    },
    {
      "gtm.start": 1782286063647,
      "event": "gtm.js"
    }
  ]
}

The first result, "exists": true, means the website has a dataLayer available in the browser. In simple terms, the website has created the object that Google Tag Manager can read from.

That is already useful information because, without a dataLayer, there would be no central place where the website can push structured events, consent signals, or business information for GTM to process.

The second result, "isArray": true, means the dataLayer is an array.

That might sound technical, but the idea is simple: the dataLayer behaves like a list. Every time the website wants to communicate something to GTM, it adds a new item to that list.

For example, it can add a page load, a consent setting, a purchase, a search, a button click, or any other event the business wants to measure.

The "length": 4 result means that, at the moment I checked the page, the dataLayer already had four entries inside it.

I had not clicked the cookie banner yet. I had not searched anything. I had not bought a ticket. Still, the website had already pushed four pieces of information into the dataLayer.

This is an important detail. The dataLayer is not only used for clicks and purchases. It can also be used for setup instructions, consent rules, and the initial GTM load event.

The "eventNames": ["gtm.js"] result means that, out of those four entries, the only entry with a clear event name is gtm.js.

gtm.js is the event that Google Tag Manager adds when the GTM container starts loading on the page. It is basically GTM saying: “I have started.”

This is not a business event like “ticket purchased” or “guided tour selected.” It is more like a technical startup signal.

That is why the result only shows one event name. The other entries exist in the dataLayer, but they are not written as normal website events. They are commands.

The first entry is:

{
  "0": "consent",
  "1": "default",
  "2": {
    "ad_storage": "granted",
    "ad_user_data": "granted",
    "ad_personalization": "granted",
    "analytics_storage": "granted",
    "personalization_storage": "granted",
    "functionality_storage": "granted",
    "security_storage": "granted",
    "wait_for_update": 2000
  }
}

This looks strange because it has "0", "1", and "2" instead of names like "event" or "page_type".

But the logic is simple. This is a gtag command stored inside the dataLayer.

The gtag() command is a JavaScript function used in Google’s measurement and advertising setup. The consent command manages user privacy choices. It sets default permissions and updates them, for example from denied to granted, when a user interacts with a cookie banner.

The first part, "0": "consent", means the command is related to consent.

The second part, "1": "default", means it is setting the default consent state. Before the user chooses anything on the cookie banner, the website is telling Google what the default consent behavior should be.

The third part contains the actual consent settings. These values say that, by default, storage for ads, analytics, personalization, functionality, and security is granted.

However, this is only the first consent rule. Right after that, the website adds a second consent rule.

{
  "0": "consent",
  "1": "default",
  "2": {
    "ad_storage": "denied",
    "ad_user_data": "denied",
    "ad_personalization": "denied",
    "analytics_storage": "denied",
    "personalization_storage": "denied",
    "functionality_storage": "denied",
    "security_storage": "denied",
    "wait_for_update": 2000,
    "region": ["AT", "BE", "FR", "DE", "IT", "ES", "SE", "GB"]
  }
}

This is also a consent default command, but now there is a very important extra field: region.

This means the rule applies only to specific countries. These are country codes. For example, ES means Spain, FR means France, DE means Germany, IT means Italy, and GB means Great Britain.

So when I see this region list, I know the website is applying a stricter default consent behavior for visitors from those countries.

Since Spain is included as ES, this matters directly for a website like Sagrada Família, where many users will visit from Barcelona, Spain, or other European countries.

Inside that regional rule, almost everything is set to denied. This means that, for users in those listed regions, the default state is more restrictive before the user makes a choice on the cookie banner.

So the website is basically saying:

  • For general users, use granted consent as the default.
  • For users in these listed regions, use denied consent as the default.

That is how we can see that the site is treating European or privacy-regulated traffic differently before the visitor accepts or rejects the cookie banner.

The value "wait_for_update": 2000 tells Google tags to wait up to 2000 milliseconds, which is 2 seconds, for a consent update.

The website is giving the cookie banner a small window of time to collect the user’s choice before the tags continue. This does not mean the user is forced to wait exactly 2 seconds. It means Google’s tagging system has a short delay available so the consent state can be updated properly.

The "set" command with "developer_id.dNjAwYj": true looks like an internal Google or tag vendor identifier. It is not telling us that a user clicked something, searched something, or bought something.

Finally, "event": "gtm.js" means the GTM container has started. This is usually one of the first events you see when checking a website with Google Tag Manager.

What do the consent options mean?

Now that we have seen how the dataLayer behaves, let’s look at the consent options.

Consent field What it means in this kind of audit
ad_storage The website can use advertising cookies. For example, this can help Google understand that a visit came from an ad and later whether that visit became a ticket purchase. In the browser, this can create cookies like _gcl_au.
ad_user_data The website has permission to send advertising-related user data to Google, usually for conversion measurement, depending on the implementation.
ad_personalization The website can use the visitor’s behavior for personalized ads or remarketing.
analytics_storage Analytics cookies can be used. This helps tools like GA4 understand the user journey more clearly.
personalization_storage The website can remember information that improves the user experience, such as previous choices or preferences.
functionality_storage The website can remember practical settings that make the site easier to use, such as selected language, region, currency, or interface preference.
security_storage The website can use storage for security and protection, such as authentication, fraud prevention, or keeping checkout sessions safe.

What happened after waiting a few more seconds?

I decided to run the script again, but with a small change: I waited 6 extra seconds to see if the dataLayer would show more items on the website.

And it did.

I obtained all consent events listed:

  • Consent Necessary, set to true
  • Consent Advertising, set to false
  • Consent Performance, set to false
  • Consent Functional, set to false
  • After Consent Update
  • Init
  • Layer Ready
"afterSixSeconds": {
  "exists": true,
  "isArray": true,
  "length": 11,
  "eventNames": [
    "gtm.js",
    "cf_consent_necessary",
    "cf_consent_advertising",
    "cf_consent_performance",
    "cf_consent_functional",
    "cf_after_consent_update",
    "cf_init",
    "cf_layer_ready"
  ],
  "consentEvents": [
    {
      "event": "cf_consent_necessary",
      "cf_necessary_enabled": true
    },
    {
      "event": "cf_consent_advertising",
      "cf_advertising_enabled": false
    },
    {
      "event": "cf_consent_performance",
      "cf_performance_enabled": false
    },
    {
      "event": "cf_consent_functional",
      "cf_functional_enabled": false
    }
  ]
}

That matches the banner UI: necessary is already on and cannot really be disabled, while the other categories are off until the user chooses.

Matching the banner UI with the actual consent state is essential for privacy QA, because the interface and the tracking behavior should tell the same story.

Sagrada Familia Cookie Banner
Sagrada Familia Cookie Banner when opening the website.

What happens to the dataLayer after accepting all cookies?

Before the click, the consent events showed necessary (the minimum functional cookies for the website to run) as true, while advertising, performance, and functional were false.

After accepting, new consent events were pushed and those same optional categories became true.

The dataLayer length increased from 11 to 19, and extra GTM events appeared, including gtm.dom and gtm.scrollDepth.

{
  "event": "cf_consent_advertising",
  "cf_advertising_enabled": true,
  "gtm.uniqueEventId": 17
},
{
  "event": "cf_consent_performance",
  "cf_performance_enabled": true,
  "gtm.uniqueEventId": 18
},
{
  "event": "cf_consent_functional",
  "cf_functional_enabled": true,
  "gtm.uniqueEventId": 19
}

This snippet shows an area where many people get confused, including me.

Even after accepting all cookies, the dataLayer still contains the earlier default consent events where optional categories were false. But later entries show advertising, performance, and functional changing to true.

This matters because the dataLayer keeps a timeline of what happened, rather than only showing the final consent state.

A useful rule:

During the same page visit, the dataLayer is usually not refreshed. It is appended to. It grows like a list of events and commands.

On a page reload, it starts again, but the new dataLayer may be rebuilt using saved cookies and localStorage.

If the website is a single-page app, where the page does not fully reload and only the content changes, then it is usually the same dataLayer. In that case, the list keeps growing.

If checkout opens inside an iframe or redirects to another domain, then it can get more complex. The parent page has its own window.dataLayer, and the iframe or new domain may have a separate one. So the checkout may not share the same dataLayer unless the implementation sends events between them.

This depends on the implementation.

So how are cookies and dataLayers talking to each other?

When cookies were accepted, the consent decision was stored in localStorage. That is one observation.

When cookies were accepted, the dataLayer also received new updated events such as cf_consent_advertising, cf_consent_performance, and cf_consent_functional, all equal to true.

You can see that these events are processed by GTM because they include values like this:

"gtm.uniqueEventId": 17

So, are cookies working as expected?

In this step, yes.

But this does not automatically prove that a GA4 tag, Google Ads tag, or remarketing tag fired from that event.

To prove tag firing, you would check GTM Preview or Tag Assistant. You could also inspect network requests after consent is accepted, for example requests to:

  • google-analytics.com/g/collect
  • googleadservices.com
  • doubleclick.net

Google also explains that Tag Manager processes dataLayer messages in order, one at a time. So the sequence matters: first the default consent state, then the updated consent state after the user makes a choice.

That is why auditing consent means checking three layers:

  • What the banner says.
  • What the browser stores.
  • What the dataLayer sends to GTM.

If those three layers are aligned, the website is much easier to trust and debug.

If they are not aligned, you may have a tracking problem, a consent problem, or both.

Happy to answer questions about this topic at matteoarellano.com.

Sources and further reading