
Why you should NOT use the OneTrust GTM template
In GTM, it is best practice to use community templates because these are maintained by vendors, are easier for marketers to edit, and are more secure. This is due to using a JS sandbox with DOM evaluation & manipulation disabled.
However, we will make an exception to this best practice regarding the OneTrust GTM template because it has 3 critical issues, and thus, we recommend using custom HTML instead.
This post aims to make OneTrust aware of these issues so that they fix them. We also want to inform users of the workaround while an update is pending.
Issues
1. Determining language from the page’s HTML
Firstly, the OT template does not support an override for determining language from the page’s HTML, such as lang=en. It only supports using the language set in the user’s browser. This could result in a web page in English but the banner in German, etc. Thus, some website owners force the banner language to be the same as the web page. If you are using this feature, unfortunately, you can not use the OT template 🙁
ot.setAttribute("data-document-language", true);
This is the most requested feature on the GitHub issue board, and it’s often requested in the OT developer forum.
2. HTML5 data attributes are not supported in sandbox JS
Secondly, HTML5 data attributes are not supported in sandbox JS for security reasons. Thankfully, there is a solution! Taking the example of the OT domainID this can be set using either using a param in the script src such as ?did=xxxx or via an HTML5 attribute data-domain-script=xxxx. This dual method works because otSDKStub.js has been set to read both methods. However, other attributes such as data-document-language, data-ot-ignore or class=optanon-category-C0001 do NOT support this dual-mode because OT developers have not added dual-method support for these params 🙁
// Example of OT dual-method for domainID using either &did="xxxx" or data-domain-script="xxxx" within otSDKStub.js
function setStubScriptElement() {
return document.querySelector("script[src*='otSDKStub.js']").getStubQueryParam("did") ||
document.querySelector("script[data-domain-script]").getAttribute("data-domain-script");
}Thus, if you want to activate advanced features such as data-dlayer-ignore=”true”, you are forced to use a Custom HTML tag in GTM rather than the OT community template 🙁
The OT banner loader otSDKStub.js is set to render dataLayer.event=OneTrustGroupsUpdated after either a banner-loaded event or an accept/decline interaction. However, this is different to other CMP’s such a Cookiebot which only send dataLayer.event=cookie_consent_update after an accept/decline (not banner loaded).
For GCM modelling to work correctly, the deny cookieless pings and granted cookie pixels should be sent on accept/decline only. They should NOT be sent onload for new users. Sending OneTrustGroupsUpdated when there has been no user interaction is incorrect, as it should wait for a positive opt-in or opt-out signal from the user.
To correct this issue, MeasureMinds have created a JS patch which disables the standard OT dataLayer integration using data-dlayer-ignore=true (this also stops the legacy OptanonLoaded & OneTrustLoaded events which should never be used).
<!-- Example of dlayer-ignore="true" --> <script src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js?did=fff8df06-1dd2-491b-88f6-01cae248cd17" async data-dlayer-ignore="true"> </script>
Once the data-dlayer-ignore is disabled, we attach a new dataLayer event to OneTrust. OnConsentChanged() which sends accept/decline and saves these values in a localStorage key called gtm_consentMode, so that they can be read on the second page onwards.
We read the existing OptanonConsent cookie and convert this into consentModeOBJECT, so there is no need to force a reconsent within the OT tool.
The advantages of this solution:
- It’s more compliant as only 1 deny ping is sent rather than 2 pings, and the deny ping is only sent after the user has read the Banner message.
- It allows us to send the OneTrustGroupsUpdated at the correct time so that existing GTM triggers do not need to be updated.
- There are fewer dataLayer eval() events because 3 event pushes of OneTrustGroupsUpdated, OptanonLoaded, and OneTrustLoaded are removed.
- If you are using tags that run on once-per-page (rather than once-per-event), these will be more reliable as there won’t be a false positive on the first OneTrustGroupsUpdated event.
- If you plan to migrate between Onetrust to Cookiebot or vice versa, the set-ups will be more standardised, and it’s easier to change OneTrustGroupsUpdated to cookie_consent_update or vice versa.
- The MM script waits 500ms for the GSM to set before sending updateConsent dataLayer push, and thus, there is no need for OneTrustGroupsUpdatedConsentSaved or isOnetrustActive workarounds.
Hope you fine this helpful, pls add questions in the comments below.
Thanks
Phil & MM team
Here is the Custom HTML tag…
var gtm_isDEV = false; // Change to true to enable console log
/************************************************/
/* Function: getCookie */
/************************************************/
function gtm_getCookie(CookieName) {
"use strict";
var output="";
if(CookieName && CookieName!=="undefined" && CookieName!=="null") {
var cookieValue = "; " + document.cookie;
var cookieParts = cookieValue.split("; " + CookieName + "=");
if (cookieParts && cookieParts.length===2) {
output = decodeURIComponent( cookieParts.pop().split(";").shift() );
}
//if (gtm_isDEV===true && output) console.log("Msg: getCookie " + CookieName + ": " + decodeURIComponent(cookieValue) );
return output;
}
}
// Set DEFAULT consent
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag("consent", "default", {
"analytics_storage": "denied",
"personalization_storage": "denied",
"functionality_storage": "denied",
"ad_storage": "denied",
"ad_personalization": "denied",
"ad_user_data": "denied",
"security_storage": "granted"
});
// gtag("set", {developer_id.dYWJhMj: true});
// gtag("set", "ads_data_redaction", true);
// gtag("set", "url_passthrough", true);
// Inject Onetrust otSDKStub.js
(function(){
var gtm_onetrust_id = "fff8df06-1dd2-491b-88f6-01cae248cd17"; // Replace with your domainID here
if (gtm_isDEV===true) {
gtm_onetrust_id = gtm_onetrust_id + "-test"; // Append the word "-test" on DEV only
}
var _ot = document.createElement("script");
_ot.src = "https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"; // If using CookiePro use: cookie-cdn.cookiepro.com/scripttemplates/otSDKStub.js
_ot.type = "text/javascript";
_ot.charset = "UTF-8";
_ot.async = true;
_ot.setAttribute("data-domain-script", gtm_onetrust_id);
_ot.setAttribute("data-ot-ignore", ""); // For autoBlock category
_ot.classList.add("optanon-category-C0001"); // For autoBlock category
_ot.setAttribute("data-dlayer-ignore", "true"); // Disabled standard GTM dataLayer integration
// _ot.setAttribute("data-document-language", "true"); // Enabled page language (not browser language)
var _s = document.getElementsByTagName("script")[0]; _s.parentNode.insertBefore(_ot, _s);
})();
/*
OneTrust Consent Changed/Updated Listener...
Note: Enable OneTrustDebug mode using: OneTrust.testLog();
//my.onetrust.com/s/article/UUID-d8291f61-aa31-813a-ef16-3f6dec73d643?language=en_US
//www.youtube.com/watch?v=MqAEbshMv84
//github.com/googleanalytics/ga4-tutorials/blob/main/src/public/layouts/layout.eta
//github.com/googleanalytics/ga4-tutorials/blob/main/src/public/partials/consent.eta
*/
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Consent enums
var CONSENT = {
denied: "denied",
granted: "granted"
};
function isConsented(logicalTest){
return logicalTest ? CONSENT.granted : CONSENT.denied;
}
function isCategoryConsentInCookie(cookie, otCategoryId){
return isConsented( cookie.indexOf(otCategoryId+":1") > -1 );
}
function updateConsent(consentModeOBJECT, wait_for_update, isDEV){
gtag("consent", "update", consentModeOBJECT );
if(!wait_for_update) wait_for_update = 500;
setTimeout(function (){
dataLayer.push({
"event": "OneTrustGroupsUpdated",
"consent_object": consentModeOBJECT
// ,"isGCEnabled": OneTrust.GetDomainData().GoogleConsent.GCEnable
});
}, wait_for_update); // 0.5sec
if(isDEV===true) console.log("GTM msg: gcm.updateConsent: ", consentModeOBJECT);
}
// Onetrust Callback function
var OptanonWrapperFunction = function () {
var gtm_consentModeOBJECT = {};
var gtm_consentModeSTRING = null;
var gtm_hasOnConsentChangedRun = false; // Assume NOT using "use strict" and OK to set outside function scope
if(typeof OneTrust==="object" && typeof OneTrust.OnConsentChanged==="function") {
// 1. START - ConsentCHANGED Listener - Save value in local storage
OneTrust.OnConsentChanged(function(e) {
if(gtm_isDEV===true) console.log("GTM msg: OnConsentChanged.detail: ", e.detail);
var statistics = e.detail.includes("C0002") || e.detail.includes("2") ? "granted" : "denied";
var preferences = e.detail.includes("C0003") || e.detail.includes("3") ? "granted" : "denied";
var marketing = e.detail.includes("C0004") || e.detail.includes("4") ? "granted" : "denied";
gtm_consentModeOBJECT = {
"analytics_storage": statistics || "denied",
"personalization_storage": statistics || "denied",
"functionality_storage": preferences || "denied",
"ad_storage": marketing || "denied",
"ad_personalization": marketing || "denied",
"ad_user_data": marketing || "denied",
"security_storage": "granted"
};
if(gtm_isDEV===true) console.log("GTM msg: OnConsentChanged: ", gtm_consentModeOBJECT);
updateConsent( gtm_consentModeOBJECT, 500, gtm_isDEV); // Includes 500ms wait_for_update
// SET values to local storage
gtm_consentModeSTRING = JSON.stringify( gtm_consentModeOBJECT );
if(gtm_consentModeSTRING) {
if(typeof localStorage==="object") localStorage.setItem("gtm_consentMode", gtm_consentModeSTRING);
// if(typeof gtm_setCookie==="function") gtm_setCookie("gtm_consentMode", gtm_consentModeSTRING, 365);
}
// Prevent loading twice onload and onclick
gtm_hasOnConsentChangedRun = true;
});
// Update with previous consent from OneTrust cookie if it already exists
var gcmMapping = [
{
"gcmCategory": "security_storage",
"oneTrustCatId": "C0001",
"defaultConsent": "On by default"
},{
"gcmCategory": "analytics_storage",
"oneTrustCatId": "C0002",
"defaultConsent": "Off by default"
},{
"gcmCategory": "personalization_storage",
"oneTrustCatId": "C0002",
"defaultConsent": "Off by default"
},{
"gcmCategory": "functionality_storage",
"oneTrustCatId": "C0003",
"defaultConsent": "Off by default"
},{
"gcmCategory": "ad_storage",
"oneTrustCatId": "C0004",
"defaultConsent": "Off by default"
},{
"gcmCategory": "ad_personalization",
"oneTrustCatId": "C0004",
"defaultConsent": "Off by default"
},{
"gcmCategory": "ad_user_data",
"oneTrustCatId": "C0004",
"defaultConsent": "Off by default"
}
];
var consentCookie = gtm_getCookie("OptanonConsent"); // e.g. "&groups=1:1,2:1,3:0,4:0&" or {{Cookie - OptanonConsent}}
var OptanonAlertBoxClosed = gtm_getCookie("OptanonAlertBoxClosed"); // e.g. "2024-04-22T10:31:32.252Z" or {{Cookie - OptanonAlertBoxClosed}}
if(OptanonAlertBoxClosed && consentCookie) {
var previousConsent = {};
gcmMapping.forEach( function(e) {
previousConsent[e.gcmCategory] = isCategoryConsentInCookie(consentCookie, e.oneTrustCatId);
});
gtm_consentModeOBJECT = previousConsent;
if(gtm_isDEV===true) console.log("GTM msg: cookie.OptanonConsent: ", previousConsent);
}
var otActiveGroups = window.OnetrustActiveGroups;
var consentArray = []; if (otActiveGroups) consentArray = otActiveGroups.split(",");
var updateData = {};
// TBC: Ignore default opt-out
if(otActiveGroups && otActiveGroups!==",C0001," && otActiveGroups!==",1,") {
gcmMapping.forEach( function(e) {
if( !e.oneTrustCatId ){
// console.log("GTM msg: Skipping update for " + e.gcmCategory + " because a valid OneTrust ID was not provided in GTM.");
return; // Abandon function oneTrustCatId missing
}
updateData[e.gcmCategory] = isConsented(consentArray.indexOf(e.oneTrustCatId) > -1);
});
gtm_consentModeOBJECT = updateData;
gtm_consentModeSTRING = JSON.stringify( gtm_consentModeOBJECT );
if(typeof localStorage==="object") localStorage.setItem("gtm_consentMode", gtm_consentModeSTRING);
if(gtm_isDEV===true) console.log("GTM msg: window.OnetrustActiveGroups: ", updateData);
}
// 3. START - GET from local storage
gtm_consentModeSTRING = localStorage.getItem("gtm_consentMode");
if( gtm_consentModeSTRING==null ) {
// if consent cookie null - do nothing as consent NOT BEEN SET BY USER
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST: a. do nothing as consent NOT BEEN SET BY USER");
} else {
// Poll for OneTrust.OnConsentChanged to only run when its READY
var onetrustReady = function() {
if(gtm_isDEV===true) console.log("GTM msg: gtm_hasOnConsentChangedRun: " + gtm_hasOnConsentChangedRun);
if ( gtm_hasOnConsentChangedRun===true ) {
// if consent already sent - do nothing as consent ALREADY SENT
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST: b. do nothing as consent ALREADY SENT");
} else if ( gtm_consentModeSTRING ) {
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST: c. SENT onload consent");
gtm_consentModeOBJECT = JSON.parse( gtm_consentModeSTRING );
updateConsent( gtm_consentModeOBJECT, 500, gtm_isDEV); // Includes 500ms wait_for_update
}
};
// Polling function to check if OneTrust is ready
var i = 0;
var checkIsOnetrustReady = function() {
i = i + 1;
if (typeof OneTrust==="object" && typeof OneTrust.OnConsentChanged==="function") {
// STOP the polling
onetrustReady();
clearInterval(pollforOnetrustReady);
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST isReady polling... counter was: " + i);
} else if (i > 500) {
// Give up after 5sec (5000ms)
clearInterval(pollforOnetrustReady);
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST isReady polling... onetrust NOT ready after 5 seconds");
}
};
// START polling every 10ms
var pollforOnetrustReady = setInterval(checkIsOnetrustReady, 10);
if(gtm_isDEV===true) console.log("GTM msg: ONETRUST isReady polling started");
}
// 3. END - GET from local storage
}
};
// Assign the function to a global variable to ensure accessibility
window.OptanonWrapper = OptanonWrapperFunction;Side note: Denied hits are re-processed after consent granted onload and then the same hit on accept. Thus GA4 accounts for this scenario. But Bing consent mode and other vendors do not account for this and it not an ethical solution.
- How to Run a Google Tag Manager (GTM) Audit - 26/11/2025
- How to Run a Web Analytics Audit: Examples & Tools - 30/10/2025
- How to Run a Cookie Audit: Examples and Tools - 23/10/2025
As a side note… its also possible to clean-up the dataLayer when using GEO-IP to use OT GEO-IP rather than Google. This makes the GTM debug view look much cleaner and avoid 27 push for each EU country However, its necessary to wait for otSDKStub.js to load, then send the OptanonWrapper() callback and edit the updateConsent() function to digest otStubData.userLocation.country and otStubData.userLocation.state as function inputs. This is a draft version, it does not include the new updateConsent with digests yet (I`ll update soon). But I wanted to log this as a V2 script improvement. <script> var gtm_isDEV = false; //… Read more »