Why you should NOT use the OneTrust GTM template

Phil Pearce
First published April 25th, 2024
Last updated May 1st, 2024
We found three issues with using the OneTrust GTM template. Find out more details and a custom HTML tag to fix the problem instead.
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.


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") || 

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 🙁

3. Deny cookieless pings and granted cookie pixels are being sent onload for new users

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">

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:

  1. 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.
  2. It allows us to send the OneTrustGroupsUpdated at the correct time so that existing GTM triggers do not need to be updated.
  3. There are fewer dataLayer eval() events because 3 event pushes of OneTrustGroupsUpdated, OptanonLoaded, and OneTrustLoaded are removed.
  4. 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.
  5. 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.
  6. 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.


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
    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();
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 (){
            "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.

Phil Pearce
Follow me
0 0 votes
Article Rating
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Newest Most Voted
Inline Feedbacks
View all comments
Articles from our Blog
Would love your thoughts, please comment.x