From Noob to Maxed: Unlocking GTM with Cookie Consent

Whilst never the most exciting part of my job as a front-end web engineer, user consent has been a commonly misunderstood and unappreciated aspect of it. It is still a pet hate of mine seeing the anti-patterns in cookie consent pop-ups as you browse the web. Many websites bend the rules to make it harder to opt out or make you opt in if you want to read their news article.
Cookie policies on the internet are not new. The EU's E-Privacy Directive of 2002 required that website visitors be given certain information about cookies. While not particularly stringent, this was the origin of the classic: "by using this website, you agree to the use of cookies for yada yada".
I remember implementing this on the RuneScape websites as a little pop-up in the corner that would be quickly dismissed. As much as I found it annoying at the time, the principle behind it was admirable, if just a little out of touch. Over the years, the policy has been refined and tightened, so that EU traffic (with some minor exceptions) is now opt-in only for anything other than necessary functionality for the site to work.
Google Tag Manager has existed since 2012. For those of you who do not know, it is a tool that lets you add, manage, and update tracking code ("tags") on your website or app without needing to edit the site's code every time. This is very useful for marketing teams that often run multiple ad campaigns and want to track their performance and return on investment.
Alas, while marketing teams love it, it can be a concern for developers. It is theoretically possible to ship breaking JavaScript straight to production and tank your website without any CI-related tests or protections. It can also add a layer of complexity when integrated with various Cookie Consent providers, such as OneTrust and Cookiebot
.
When we integrated OneTrust as our new consent provider, I found the GTM integration challenging. I hope this serves as a helpful guide for my fellow developers.
Deciding the approach
There are essentially two ways to integrate Google Tag Manager to be consent-compliant, regardless of the Consent Provider you use.
Basic Setup
The simplest way is to use the consent provider on your website to block the GTM snippet from running until a condition is met. This is the most rigid option, as there is no way for any marketing tags to execute until a consent state is confirmed.
Advanced Consent
The second involves passing the specific consent states to GTM through the dataLayer. This gives you much more flexibility and fine-grained control inside GTM. This is loosely referred to as advance consent.
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("consent", "default", {
ad_storage: "denied",
analytics_storage: "denied",
functionality_storage: "denied",
personalization_storage: "denied",
ad_personalization: "denied",
ad_user_data: "denied",
security_storage: "granted",
wait_for_update: 1500,
});
gtag("set", "ads_data_redaction", true);Basic GTM with default consent privileges
The above snippet sets up the basic variables and functions for GTM.
window.dataLayer
The dataLayer acts as a queue, storing an array of events to be pushed to GTM when the integration is ready. This means other scripts or systems do not need to handle waiting and can push at any point during the page lifecycle where that script is executable.
Events are added to the queue by pushing them to the array; to simplify the snippet, I prefer to make this a small function called gtag.
consent default
Next, we set up a default state for the GTM consent model. This must run before any events are sent or tags fired. By default, all relevant Google consent types are denied unless you have a valid reason to set them to granted. The exception is security_storage, which is used for fraud prevention or bot detection, not for marketing or analytics tracking.
The wait_for_update property instructs GTM to wait X milliseconds before firing tags, in case it can obtain a consent state in advance. This happens when consent has already been granted, and you are just waiting for the consent provider SDK to initialise and provide it.
The gtag action gtag("set", "ads_data_redaction", true) strips certain advertising data from requests and is generally considered a good practice.
Syncing Consent
We now need to synchronise our Consent provider (in my case, OneTrust) with GTM Consent.
(function(){
function hasGroup(id) {
return typeof window.OnetrustActiveGroups === "string" &&
window.OnetrustActiveGroups.indexOf(id) > -1;
}
function syncGCM(){
if (typeof window.gtag !== "function") return;
if (typeof window.OnetrustActiveGroups !== "string") return;
gtag("consent", "update", {
ad_personalization: hasGroup("C0004") ? "granted" : "denied",
ad_storage: hasGroup("C0004") ? "granted" : "denied",
ad_user_data: hasGroup("C0004") ? "granted" : "denied",
analytics_storage: hasGroup("C0002") ? "granted" : "denied",
functionality_storage: hasGroup("C0003") ? "granted" : "denied",
personalization_storage: hasGroup("C0003") ? "granted" : "denied",
security_storage: "granted",
});
dataLayer.push({event: "cookie_consent_update", onetrust_groups: window.OnetrustActiveGroups});
}
window.addEventListener("OneTrustGroupsUpdated", syncGCM, false);
syncGCM();
})();Snippet to sync the OneTrust SDK with GTM
I like to set this up as a self-invoking function that sets up properties and listeners. Inside it, we first create a hasGroup method to check whether a given consent type is available from the Consent Provider. This checks whether the provider's window object confirms that the user has consented to the given type (marketing, performance, etc.). You will need to tailor this to your chosen consent provider SDK.
The syncGCM function is the key part of this snippet. Its job is to push the current consent state to GTM. It first checks that any functions or properties it needs are available. Following this, it then pushes an event similar to the default consent event, but this time with the property "update" and the other properties for Google consent types mapped to your consent provider. We also send another event labelled "cookie_consent_update" that includes the raw Consent Provider details, which we will use later in GTM.
Finally, we set up a listener for every change to the Consent Provider's consent object so we can re-update Google, then call the syncGCM function once to ensure it runs on page load.
Add the tag manager snippet
The final piece of script you need to add is the one that loads the GTM system into the page.
(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.setAttributeNode(d.createAttribute("data-ot-ignore"));
j.async = true;
j.src =
"https://www.googletagmanager.com/gtm.js?id=" + i + dl;
f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", "<YOUR GTM CONTAINER ID>");The snippet of JS provided by GTM to load the script into the page
This code is provided by GTM and can be added directly to the page after the consent code. Make sure your GTM container ID is set to your container ID. You can download it from your GTM workspace by clicking your container ID (GTM-XXXXX) in the top-right.

If you are using advanced consent, ensure that this script and the script tag it creates are not blocked by your consent provider!
Setting up Tag Manager
Now that your web page is loading GTM, you will need to configure your tags to fire at the right time and under the right conditions. It is essential for compliance that you identify the purpose of each tag and how it uses the information it collects. If you are unsure of the full use of the data but need to use the tag, you should always assume it requires all consent purposes.
Data Layer Variable
First, we will create a new user-defined Data Layer variable from the variables tab on the left. We will call it "OneTrustActiveGroups" and set it up as follows.

This allows us to read the window.OneTrustActiveGroups variable in the browser, which is set by our CMP. We will need this to determine the current consent state when the user updates it (or when it is initially set after the page loads).
The tag configuration
Next up, we'll set up the tag we want to fire under the right consent conditions.

You will see a generic tag added to our workspace. Clicking the tag section (outlined in blue) will allow you to edit the tag's settings. The section we are interested in is labelled "Consent Settings". Here, you should choose "Additional Consent Checks", and you will then see further options. This is where you can set the Google consent purposes required for your tag to fire. You might recognise the types from the snippets above.
- ad_personalization
- ad_storage
- ad_user_data
- analytics_storage
- functionality_storage
- personalization_storage
- security_storage
Adding one of these to your list means that your GTM snippet code must set that setting to "granted" before this tag can fire.

Saving this means the tag will now run only when the consent settings are met, but we also need to set up the trigger. This is because we probably will not have consent on page load, as the script runs asynchronously and your CMP SDK (OneTrust) may not have been available yet.
Next, we need to set the Tag firing options to "Once per page" to ensure we do not trigger multiple events on a single view.
The trigger
Finally, we must create a new trigger for this tag and call it something like "Consent Update (all pages)". We can use this as a trigger that fires when our desired consent state is reached (in this example, advertising or targeted cookies), defined as C0004 in OneTrust.

The settings above tell the trigger to fire if the cookie_consent_update event is detected and the OneTrustActiveGroups variable contains C0004 (advertising cookies have been accepted in OneTrust). Combined with the Once per page setting on the tag, this ensures the tag fires only once on page load, if and when consent is given.
This will need further customisation if you want to support analytical tags that do not require ad_storage, but I have found that nearly all tags I have used in GTM require ad_storage. This acts as a good starter for 10.
Some of the foot-guns
I encountered various issues during this process, and I hope declaring these here helps someone in the future solve problems they are having.
The Chrome Tag Assistant Plugin reports that a CMP is blocking GTM.
This is often a red herring, as the plugin encounters many issues. In general, I would just use the debugger inside GTM, although you will need to ensure your Content Security Policy allows tag manager requests.
Only some events are coming through, far fewer than expected.
There will be some drop when properly integrating a CMP and GTM, especially if you have never used one before. Be sure you have correctly set up your CMP template with the correct GeoLocation rules and that all consent groups are included so users can opt in.
The snippets do not always run.
If you are using autoblock on your CMP, ensure that your GTM snippets are excluded if you are using advanced consent.
In Conclusion
The above is by no means a perfect example of how to use GTM and I fully advise anyone doing this to consider building a wrapper or library to standardise this across your tech stack. Consent for storage is a confusing exercise to understand and implement and working close with DPOs and legal counsel can prove vital to getting it right.
Try not to panic about the task at hand - if in doubt go with the process of least risk to you or the business and avoid anti-patterns of tricking users into consenting. I hope one day this sort of thing is managed at a browser level removing the need for these custom setups. But until that time it is our responsibility to be responsible with our customer's choices. I'd rather let them protect their identify and habits than sell it for the sake of some basic analytical gain.