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" // "wait_for_update": 500 }); // 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); // 500 ms if(isDEV===true) console.log("GTM msg: gcm.updateConsent: ", consentModeOBJECT); } // Onetrust Callback function function OptanonWrapper() { var gtm_consentModeOBJECT = {}; var gtm_consentModeSTRING = null; 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" }; 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); } }); // Update with previous consent from OneTrust cookie if it already exists var gcmMapping = [ { "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 } else if ( gtm_consentModeSTRING ) { gtm_consentModeOBJECT = JSON.parse( gtm_consentModeSTRING ); if(gtm_isDEV===true) console.log("GTM msg: localStorage.gtm_consentMode: ", gtm_consentModeOBJECT) updateConsent( gtm_consentModeOBJECT, 500, gtm_isDEV); // Includes 500ms wait_for_update } } }
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.
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 avoice 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 digersts yet (I`ll update soon). But I wanted to log this as a V2 script improvement. <script> var gtm_isDEV = false; // Change… Read more »