diff --git a/media/js/base/consent/utils.es6.js b/media/js/base/consent/utils.es6.js index 2cf95fad5..265709176 100644 --- a/media/js/base/consent/utils.es6.js +++ b/media/js/base/consent/utils.es6.js @@ -9,6 +9,58 @@ import MozAllowList from './allow-list.es6'; const COOKIE_ID = 'moz-consent-pref'; // Cookie name const COOKIE_EXPIRY_DAYS = 182; // 6 months expiry +/** + * Sets GTAG ads consent mode + * @param {Boolean} hasConsent - if analytics pref is true or false + * @param {String} type - one of consent mode types (default|update) + * @returns {Boolean} + */ +function setGtagAdsConsentMode(hasConsent, type = 'update') { + // bail out if GTAG has not been created with GTMSnippet.loadSnippet + // this needs to run before GTM snippet loads to set proper defaults + if (typeof window.gtag === 'undefined') { + return false; + } + if (hasConsent) { + window.gtag('consent', type, { + ad_user_data: 'granted', + ad_personalization: 'granted', + ad_storage: 'granted' + }); + } else { + window.gtag('consent', type, { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + } + return true; +} + +/** + * Sets GTAG analytics consent mode + * @param {Boolean} hasConsent - based on /landing/get default or analytics cookie + * @param {String} type - one of consent mode types (default|update) + * @returns {Boolean} + */ +function setGtagAnalyticsConsentMode(hasConsent, type = 'update') { + // bail out if GTAG has not been created with GTMSnippet.loadSnippet + // this needs to run before GTM snippet loads to set proper defaults + if (typeof window.gtag === 'undefined') { + return false; + } + if (hasConsent) { + window.gtag('consent', type, { + analytics_storage: 'granted' + }); + } else { + window.gtag('consent', type, { + analytics_storage: 'denied' + }); + } + return true; +} + /** * Determines if the current page requires consent. * Looks for a data attribute on the tag. @@ -106,6 +158,9 @@ function setConsentCookie(data) { 'lax' ); + setGtagAdsConsentMode(data.analytics); + setGtagAnalyticsConsentMode(data.analytics); + return true; } catch (e) { return false; @@ -135,6 +190,18 @@ function isFirefoxDownloadThanks(location) { return location.indexOf('/thanks/') > -1; } +/** + * Determine if the current page is /landing/get. + * @param {String} location - The current page URL. + * @return {Boolean}. + */ +function isFirefoxLandingGet(location) { + if (typeof location !== 'string') { + return false; + } + return location.indexOf('/landing/get') > -1; +} + /** * Determines if the current page URL contains a query string * that allows the consent banner to be displayed. @@ -230,7 +297,10 @@ export { gpcEnabled, hasConsentCookie, isFirefoxDownloadThanks, + isFirefoxLandingGet, isURLExceptionAllowed, isURLPermitted, - setConsentCookie + setConsentCookie, + setGtagAdsConsentMode, + setGtagAnalyticsConsentMode }; diff --git a/media/js/base/gtm/gtm-snippet.es6.js b/media/js/base/gtm/gtm-snippet.es6.js index b43694892..6fe86ee55 100644 --- a/media/js/base/gtm/gtm-snippet.es6.js +++ b/media/js/base/gtm/gtm-snippet.es6.js @@ -9,7 +9,10 @@ import { dntEnabled, getConsentCookie, gpcEnabled, - isFirefoxDownloadThanks + isFirefoxDownloadThanks, + isFirefoxLandingGet, + setGtagAdsConsentMode, + setGtagAnalyticsConsentMode } from '../consent/utils.es6'; const GTM_CONTAINER_ID = document @@ -18,12 +21,46 @@ const GTM_CONTAINER_ID = document const GTMSnippet = {}; +if (typeof window.dataLayer === 'undefined') { + window.dataLayer = []; +} + +/** + * Set Gtag consent defaults to false unless there is a + * consent pref cookie allowing analytics OR there's no + * consent required and we're on /landing/get + */ +GTMSnippet.setGtagConsentDefaults = () => { + const cookie = getConsentCookie(); + const hasPref = cookie; + + if (hasPref) { + setGtagAdsConsentMode(cookie.analytics, 'default'); + setGtagAnalyticsConsentMode(cookie.analytics, 'default'); + } else { + setGtagAdsConsentMode(false, 'default'); + setGtagAnalyticsConsentMode( + GTMSnippet.isFirefoxLandingGet() && !consentRequired() + ? true + : false, + 'default' + ); + } +}; + /** * Load the GTM snippet. Expects `GTM_CONTAINER_ID` to be * defined in the HTML tag via a data attribute. */ GTMSnippet.loadSnippet = () => { if (GTM_CONTAINER_ID) { + window.gtag = function () { + window.dataLayer.push(arguments); + }; + // first: set default consent + GTMSnippet.setGtagConsentDefaults(); + + // then: load GTM script (the order is important) // prettier-ignore (function(w,d,s,l,i,j,f,dl,k,q){ w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});f=d.getElementsByTagName(s)[0]; @@ -41,6 +78,14 @@ GTMSnippet.isFirefoxDownloadThanks = () => { return isFirefoxDownloadThanks(window.location.href); }; +/** + * Determine if the current page is /landing/get. + * @returns {Boolean} + */ +GTMSnippet.isFirefoxLandingGet = () => { + return isFirefoxLandingGet(window.location.href); +}; + /** * Event handler for `mozConsentStatus` event. * @param {Object} e - Event object @@ -48,6 +93,10 @@ GTMSnippet.isFirefoxDownloadThanks = () => { GTMSnippet.handleConsent = (e) => { const hasConsent = e.detail.analytics; + // update gtag consent according to pref + setGtagAdsConsentMode(hasConsent); + setGtagAnalyticsConsentMode(hasConsent); + if (hasConsent) { GTMSnippet.loadSnippet(); window.removeEventListener( diff --git a/tests/unit/spec/base/consent/consent-utils.js b/tests/unit/spec/base/consent/consent-utils.js index 0d63d400b..03cb8a3f8 100644 --- a/tests/unit/spec/base/consent/consent-utils.js +++ b/tests/unit/spec/base/consent/consent-utils.js @@ -11,9 +11,12 @@ import { getHostName, hasConsentCookie, isFirefoxDownloadThanks, + isFirefoxLandingGet, isURLExceptionAllowed, isURLPermitted, - setConsentCookie + setConsentCookie, + setGtagAdsConsentMode, + setGtagAnalyticsConsentMode } from '../../../../../media/js/base/consent/utils.es6'; describe('consentRequired()', function () { @@ -361,3 +364,107 @@ describe('setConsentCookie()', function () { expect(result).toBeFalse(); }); }); + +describe('isFirefoxLandingGet()', function () { + it('should return true if URL contains /landing/get', function () { + expect( + isFirefoxLandingGet('https://www.mozilla.org/en-US/landing/get/') + ).toBeTrue(); + expect( + isFirefoxLandingGet('https://www.allizom.org/en-US/landing/get/') + ).toBeTrue(); + expect( + isFirefoxLandingGet('https://localhost:8000/en-US/landing/get/') + ).toBeTrue(); + }); + + it('should return false if URL is not /landing/get', function () { + expect( + isFirefoxLandingGet('https://www.mozilla.org/en-US/') + ).toBeFalse(); + expect( + isFirefoxLandingGet('https://www.allizom.org/en-US/') + ).toBeFalse(); + expect( + isFirefoxLandingGet('https://localhost:8000/en-US/') + ).toBeFalse(); + expect(isFirefoxLandingGet('')).toBeFalse(); + expect(isFirefoxLandingGet(null)).toBeFalse(); + expect(isFirefoxLandingGet(undefined)).toBeFalse(); + expect(isFirefoxLandingGet(true)).toBeFalse(); + }); +}); + +describe('setGtagAdsConsentMode()', function () { + afterEach(function () { + delete window.gtag; + }); + + it('should return false if window.gtag is not defined', function () { + expect(setGtagAdsConsentMode(true)).toBeFalse(); + }); + + it('should grant ads consent when called with true', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAdsConsentMode(true); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + ad_user_data: 'granted', + ad_personalization: 'granted', + ad_storage: 'granted' + }); + }); + + it('should deny ads consent when called with false', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAdsConsentMode(false); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + }); + + it('should use "default" type when specified', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAdsConsentMode(false, 'default'); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + }); +}); + +describe('setGtagAnalyticsConsentMode()', function () { + afterEach(function () { + delete window.gtag; + }); + + it('should return false if window.gtag is not defined', function () { + expect(setGtagAnalyticsConsentMode(true)).toBeFalse(); + }); + + it('should grant analytics consent when called with true', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAnalyticsConsentMode(true); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'granted' + }); + }); + + it('should deny analytics consent when called with false', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAnalyticsConsentMode(false); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'denied' + }); + }); + + it('should use "default" type when specified', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAnalyticsConsentMode(true, 'default'); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'granted' + }); + }); +}); diff --git a/tests/unit/spec/base/gtm/gtm-snippet.js b/tests/unit/spec/base/gtm/gtm-snippet.js index c440e460b..a0f30173d 100644 --- a/tests/unit/spec/base/gtm/gtm-snippet.js +++ b/tests/unit/spec/base/gtm/gtm-snippet.js @@ -162,4 +162,138 @@ describe('gtm-snippet.es6.js', function () { expect(GTMSnippet.loadSnippet).not.toHaveBeenCalled(); }); }); + + describe('GTMSnippet.setGtagConsentDefaults()', function () { + beforeEach(function () { + window.gtag = jasmine.createSpy('gtag'); + }); + + afterEach(function () { + delete window.gtag; + document + .getElementsByTagName('html')[0] + .removeAttribute('data-needs-consent'); + }); + + it('should set granted defaults when consent cookie accepts analytics', function () { + const obj = { analytics: true, preference: true }; + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue( + JSON.stringify(obj) + ); + GTMSnippet.setGtagConsentDefaults(); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'granted', + ad_personalization: 'granted', + ad_storage: 'granted' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'granted' + }); + }); + + it('should set denied defaults when consent cookie rejects analytics', function () { + const obj = { analytics: false, preference: false }; + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue( + JSON.stringify(obj) + ); + GTMSnippet.setGtagConsentDefaults(); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'denied' + }); + }); + + it('should deny all defaults when no consent cookie and not on /landing/get', function () { + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue(false); + spyOn(GTMSnippet, 'isFirefoxLandingGet').and.returnValue(false); + document + .getElementsByTagName('html')[0] + .setAttribute('data-needs-consent', 'False'); + GTMSnippet.setGtagConsentDefaults(); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'denied' + }); + }); + + it('should grant analytics default when no consent cookie, on /landing/get, and consent not required', function () { + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue(false); + spyOn(GTMSnippet, 'isFirefoxLandingGet').and.returnValue(true); + document + .getElementsByTagName('html')[0] + .setAttribute('data-needs-consent', 'False'); + GTMSnippet.setGtagConsentDefaults(); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'granted' + }); + }); + + it('should deny analytics default when no consent cookie, on /landing/get, but consent is required (EU)', function () { + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue(false); + spyOn(GTMSnippet, 'isFirefoxLandingGet').and.returnValue(true); + document + .getElementsByTagName('html')[0] + .setAttribute('data-needs-consent', 'True'); + GTMSnippet.setGtagConsentDefaults(); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'default', { + analytics_storage: 'denied' + }); + }); + }); + + describe('GTMSnippet.handleConsent()', function () { + beforeEach(function () { + window.gtag = jasmine.createSpy('gtag'); + }); + + afterEach(function () { + delete window.gtag; + }); + + it('should call gtag consent update when analytics are accepted', function () { + GTMSnippet.handleConsent({ + detail: { analytics: true, preference: true } + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + ad_user_data: 'granted', + ad_personalization: 'granted', + ad_storage: 'granted' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'granted' + }); + }); + + it('should call gtag consent update when analytics are rejected', function () { + GTMSnippet.handleConsent({ + detail: { analytics: false, preference: false } + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + ad_user_data: 'denied', + ad_personalization: 'denied', + ad_storage: 'denied' + }); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'denied' + }); + }); + }); });