From 02035057aa872946b02bcf8cca8bde40295ba044 Mon Sep 17 00:00:00 2001 From: Stephanie Hobson Date: Tue, 21 Jul 2026 16:30:23 -0700 Subject: [PATCH] Port consent updates from springfield Ports the basic consent mode configuration from springfield but does not port the download attribution or promoted pages concept. Original JIRA ticket: https://mozilla-hub.atlassian.net/browse/WT-602 Completed in: https://github.com/mozmeao/springfield/pull/955 --- media/js/base/consent/utils.es6.js | 59 ++++++++- media/js/base/gtm/gtm-snippet.es6.js | 39 +++++- tests/unit/spec/base/consent/consent-utils.js | 78 +++++++++++- tests/unit/spec/base/gtm/gtm-snippet.js | 115 ++++++++++++++++++ 4 files changed, 288 insertions(+), 3 deletions(-) diff --git a/media/js/base/consent/utils.es6.js b/media/js/base/consent/utils.es6.js index 30c8bc5321e..aec1cc0ceb5 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 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; @@ -232,5 +287,7 @@ export { isFirefoxDownloadThanks, 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 3481c2a2a9a..8a9bb4462f2 100644 --- a/media/js/base/gtm/gtm-snippet.es6.js +++ b/media/js/base/gtm/gtm-snippet.es6.js @@ -9,7 +9,9 @@ import { dntEnabled, getConsentCookie, gpcEnabled, - isFirefoxDownloadThanks + isFirefoxDownloadThanks, + setGtagAdsConsentMode, + setGtagAnalyticsConsentMode } from '../consent/utils.es6'; const GTM_CONTAINER_ID = document @@ -18,12 +20,43 @@ const GTM_CONTAINER_ID = document const GTMSnippet = {}; +if (typeof window.dataLayer === 'undefined') { + window.dataLayer = []; +} + +/** + * Set Gtag consent defaults based on consent cookie or + * visitor region. Visitors outside EU/EAA default to + * granted analytics; visitors inside EU/EAA default to + * denied until explicit consent is given. Ads are always + * denied by default unless the user has previously consented. + */ +GTMSnippet.setGtagConsentDefaults = () => { + const cookie = getConsentCookie(); + const hasPref = cookie; + + if (hasPref) { + setGtagAdsConsentMode(cookie.analytics, 'default'); + setGtagAnalyticsConsentMode(cookie.analytics, 'default'); + } else { + setGtagAdsConsentMode(false, 'default'); + setGtagAnalyticsConsentMode(!consentRequired(), '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]; @@ -48,6 +81,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 7583afa378b..5c027ed9666 100644 --- a/tests/unit/spec/base/consent/consent-utils.js +++ b/tests/unit/spec/base/consent/consent-utils.js @@ -13,7 +13,9 @@ import { isFirefoxDownloadThanks, isURLExceptionAllowed, isURLPermitted, - setConsentCookie + setConsentCookie, + setGtagAdsConsentMode, + setGtagAnalyticsConsentMode } from '../../../../../media/js/base/consent/utils.es6'; describe('consentRequired()', function () { @@ -379,3 +381,77 @@ describe('setConsentCookie()', function () { expect(result).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 set granted consent when hasConsent is 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 set denied consent when hasConsent is 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 respect an explicit type argument', 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 set granted consent when hasConsent is true', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAnalyticsConsentMode(true); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'granted' + }); + }); + + it('should set denied consent when hasConsent is false', function () { + window.gtag = jasmine.createSpy('gtag'); + setGtagAnalyticsConsentMode(false); + expect(window.gtag).toHaveBeenCalledWith('consent', 'update', { + analytics_storage: 'denied' + }); + }); + + it('should respect an explicit type argument', 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 c440e460b49..85fb30c9a1b 100644 --- a/tests/unit/spec/base/gtm/gtm-snippet.js +++ b/tests/unit/spec/base/gtm/gtm-snippet.js @@ -162,4 +162,119 @@ 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 ads and grant analytics defaults when no cookie and visitor is outside EU/EAA', function () { + spyOn(window.Mozilla.Cookies, 'getItem').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: 'granted' + }); + }); + + it('should deny all defaults when no cookie and visitor is in EU/EAA', function () { + spyOn(window.Mozilla.Cookies, 'getItem').and.returnValue(false); + 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' + }); + }); + }); });