___TERMS_OF_SERVICE___ By creating or modifying this file you agree to Google Tag Manager's Community Template Gallery Developer Terms of Service available at https://developers.google.com/tag-manager/gallery-tos (or such other URL as Google may provide), as modified from time to time. ___INFO___ { "type": "TAG", "id": "cvt_temp_public_id", "version": 1, "securityGroups": [], "displayName": "Consent Check", "categories": [ "UTILITY", "TAG_MANAGEMENT" ], "brand": { "id": "brand_dummy", "displayName": "Metric Owl", "thumbnail": "data:image/png;base64,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" }, "description": "Checks a page for the two Consent Mode mistakes behind most cookie complaints: no consent default, or one set after gtag commands; and analytics or advertising cookies present without consent. Logs the result in Preview mode and can push a consent_check event for reporting.", "containerContexts": [ "WEB" ] } ___TEMPLATE_PARAMETERS___ [ { "type": "CHECKBOX", "name": "pushEvent", "checkboxText": "Push a consent_check event to the dataLayer with the results", "simpleValueType": true, "defaultValue": true, "help": "Lets you see the result in Tag Assistant, or send it to a test GA4 property. Leave it on only while you're testing." }, { "type": "TEXT", "name": "eventName", "displayName": "Event name", "simpleValueType": true, "help": "The dataLayer event name for the results.", "defaultValue": "consent_check", "enablingConditions": [ { "paramName": "pushEvent", "paramValue": true, "type": "EQUALS" } ] } ] ___SANDBOXED_JS_FOR_WEB_TEMPLATE___ const copyFromWindow = require('copyFromWindow'); const createQueue = require('createQueue'); const isConsentGranted = require('isConsentGranted'); const getCookieValues = require('getCookieValues'); const logToConsole = require('logToConsole'); const getType = require('getType'); const ANALYTICS_COOKIES = ['_ga']; const AD_COOKIES = ['_gcl_au', '_gcl_aw', '_gcl_dc', '_fbp', '_fbc', '_uetsid', '_uetvid', 'li_fat_id', '_ttp', '_pin_unauth', '_scid']; const issues = []; // 1. Is there a Consent Mode default, and does it come before any gtag config or event command? // copyFromWindow returns undefined if the dataLayer can't be copied; say so rather than guess. const dl = copyFromWindow('dataLayer'); let defaultAt = -1; let firstCommandAt = -1; let noDefault = false; if (getType(dl) !== 'array') { issues.push('datalayer_unreadable'); } else { // Entries Tag Manager can't copy come through as undefined. If any are missing, a default // may be among them, so it's reported as unverified rather than missing. let unreadable = 0; for (let i = 0; i < dl.length; i++) { const entry = dl[i]; if (entry === undefined || entry === null) { unreadable++; continue; } if (entry[0] === 'consent' && entry[1] === 'default' && defaultAt === -1) defaultAt = i; if ((entry[0] === 'config' || entry[0] === 'event') && firstCommandAt === -1) firstCommandAt = i; } if (defaultAt === -1 && unreadable > 0) issues.push('consent_default_unverified'); else if (defaultAt === -1) { issues.push('no_consent_default'); noDefault = true; } else if (firstCommandAt > -1 && firstCommandAt < defaultAt) issues.push('gtag_command_before_consent_default'); } // 2. Are there analytics or advertising cookies without the matching consent? // isConsentGranted treats a consent type that was never set as granted, so when the dataLayer // shows no default at all, cookies count as set without consent. const analytics = noDefault ? false : isConsentGranted('analytics_storage'); const ads = noDefault ? false : isConsentGranted('ad_storage'); const stateOf = function (granted) { if (noDefault) return 'not_set'; return granted ? 'granted' : 'denied'; }; const present = function (name) { const values = getCookieValues(name); return values && values.length > 0; }; if (!analytics) { for (let i = 0; i < ANALYTICS_COOKIES.length; i++) { if (present(ANALYTICS_COOKIES[i])) issues.push('analytics_cookie_without_consent:' + ANALYTICS_COOKIES[i]); } } if (!ads) { for (let i = 0; i < AD_COOKIES.length; i++) { if (present(AD_COOKIES[i])) issues.push('ad_cookie_without_consent:' + AD_COOKIES[i]); } } const status = issues.length ? 'issues' : 'ok'; logToConsole('Consent check (Metric Owl): ' + (issues.length ? issues.join(', ') : 'no issues found'), {analytics_storage: stateOf(analytics), ad_storage: stateOf(ads)}); if (data.pushEvent) { const dataLayerPush = createQueue('dataLayer'); dataLayerPush({ event: data.eventName || 'consent_check', consent_check_status: status, consent_check_issues: issues.join(','), consent_analytics_storage: stateOf(analytics), consent_ad_storage: stateOf(ads) }); } data.gtmOnSuccess(); ___WEB_PERMISSIONS___ [ { "instance": { "key": { "publicId": "access_globals", "versionId": "1" }, "param": [ { "key": "keys", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "key" }, { "type": 1, "string": "read" }, { "type": 1, "string": "write" }, { "type": 1, "string": "execute" } ], "mapValue": [ { "type": 1, "string": "dataLayer" }, { "type": 8, "boolean": true }, { "type": 8, "boolean": true }, { "type": 8, "boolean": false } ] } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "access_consent", "versionId": "1" }, "param": [ { "key": "consentTypes", "value": { "type": 2, "listItem": [ { "type": 3, "mapKey": [ { "type": 1, "string": "consentType" }, { "type": 1, "string": "read" }, { "type": 1, "string": "write" } ], "mapValue": [ { "type": 1, "string": "analytics_storage" }, { "type": 8, "boolean": true }, { "type": 8, "boolean": false } ] }, { "type": 3, "mapKey": [ { "type": 1, "string": "consentType" }, { "type": 1, "string": "read" }, { "type": 1, "string": "write" } ], "mapValue": [ { "type": 1, "string": "ad_storage" }, { "type": 8, "boolean": true }, { "type": 8, "boolean": false } ] } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "get_cookies", "versionId": "1" }, "param": [ { "key": "cookieAccess", "value": { "type": 1, "string": "specific" } }, { "key": "cookieNames", "value": { "type": 2, "listItem": [ { "type": 1, "string": "_ga" }, { "type": 1, "string": "_gcl_au" }, { "type": 1, "string": "_gcl_aw" }, { "type": 1, "string": "_gcl_dc" }, { "type": 1, "string": "_fbp" }, { "type": 1, "string": "_fbc" }, { "type": 1, "string": "_uetsid" }, { "type": 1, "string": "_uetvid" }, { "type": 1, "string": "li_fat_id" }, { "type": 1, "string": "_ttp" }, { "type": 1, "string": "_pin_unauth" }, { "type": 1, "string": "_scid" } ] } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true }, { "instance": { "key": { "publicId": "logging", "versionId": "1" }, "param": [ { "key": "environments", "value": { "type": 1, "string": "debug" } } ] }, "clientAnnotations": { "isEditedByUser": true }, "isRequired": true } ] ___TESTS___ setup: |- const pushed = []; mock('createQueue', function () { return function (o) { pushed.push(o); }; }); const cookies = {}; mock('getCookieValues', function (n) { return cookies[n] ? [cookies[n]] : []; }); scenarios: - name: A clean page passes code: |- mock('copyFromWindow', [['consent', 'default', {}], {event: 'gtm.js'}, ['config', 'G-TEST']]); mock('isConsentGranted', false); runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_status).isEqualTo('ok'); assertThat(pushed[0].consent_check_issues).isEqualTo(''); assertApi('gtmOnSuccess').wasCalled(); - name: No consent default is reported, and cookies count as unconsented code: |- mock('copyFromWindow', [{event: 'gtm.js'}]); mock('isConsentGranted', true); cookies._ga = 'GA1.1.1.1'; runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_issues).isEqualTo('no_consent_default,analytics_cookie_without_consent:_ga'); assertThat(pushed[0].consent_analytics_storage).isEqualTo('not_set'); - name: An unreadable dataLayer is reported, and consent comes from Tag Manager code: |- mock('copyFromWindow', undefined); mock('isConsentGranted', true); cookies._ga = 'GA1.1.1.1'; runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_issues).isEqualTo('datalayer_unreadable'); assertThat(pushed[0].consent_analytics_storage).isEqualTo('granted'); assertApi('gtmOnSuccess').wasCalled(); - name: Unreadable entries make a missing default unverified, not missing code: |- mock('copyFromWindow', [undefined, {event: 'gtm.js'}]); mock('isConsentGranted', true); cookies._ga = 'GA1.1.1.1'; runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_issues).isEqualTo('consent_default_unverified'); assertThat(pushed[0].consent_analytics_storage).isEqualTo('granted'); - name: A gtag config before the default is reported code: |- mock('copyFromWindow', [['js', {}], ['config', 'G-TEST'], ['consent', 'default', {}]]); mock('isConsentGranted', false); runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_issues).isEqualTo('gtag_command_before_consent_default'); - name: Cookies without consent are reported code: |- mock('copyFromWindow', [['consent', 'default', {}]]); mock('isConsentGranted', false); cookies._ga = 'GA1.1.1.1'; cookies._fbp = 'fb.1.1.1'; runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_issues).isEqualTo('analytics_cookie_without_consent:_ga,ad_cookie_without_consent:_fbp'); assertThat(pushed[0].consent_check_status).isEqualTo('issues'); - name: Cookies are fine once consent is granted code: |- mock('copyFromWindow', [['consent', 'default', {}]]); mock('isConsentGranted', true); cookies._ga = 'GA1.1.1.1'; cookies._gcl_au = '1.1.1'; runCode({pushEvent: true, eventName: 'consent_check'}); assertThat(pushed[0].consent_check_status).isEqualTo('ok'); assertThat(pushed[0].consent_ad_storage).isEqualTo('granted'); - name: No event is pushed when the setting is off code: |- mock('copyFromWindow', [['consent', 'default', {}]]); mock('isConsentGranted', false); runCode({pushEvent: false}); assertThat(pushed.length).isEqualTo(0); assertApi('logToConsole').wasCalled(); assertApi('gtmOnSuccess').wasCalled(); ___NOTES___ Built and maintained by Marc Alexander, Metric Owl Ltd (https://www.metricowl.co.uk). Licensed under the Apache License 2.0.