Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion media/js/base/consent/utils.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Comment thread
stephaniehobson marked this conversation as resolved.
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 <html> tag.
Expand Down Expand Up @@ -106,6 +158,9 @@ function setConsentCookie(data) {
'lax'
);

setGtagAdsConsentMode(data.analytics);
setGtagAnalyticsConsentMode(data.analytics);
Comment thread
stephaniehobson marked this conversation as resolved.

return true;
} catch (e) {
return false;
Expand Down Expand Up @@ -232,5 +287,7 @@ export {
isFirefoxDownloadThanks,
isURLExceptionAllowed,
isURLPermitted,
setConsentCookie
setConsentCookie,
setGtagAdsConsentMode,
setGtagAnalyticsConsentMode
};
39 changes: 38 additions & 1 deletion media/js/base/gtm/gtm-snippet.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
dntEnabled,
getConsentCookie,
gpcEnabled,
isFirefoxDownloadThanks
isFirefoxDownloadThanks,
setGtagAdsConsentMode,
setGtagAnalyticsConsentMode
} from '../consent/utils.es6';

const GTM_CONTAINER_ID = document
Expand All @@ -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');
Comment thread
stephaniehobson marked this conversation as resolved.
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];
Expand All @@ -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(
Expand Down
78 changes: 77 additions & 1 deletion tests/unit/spec/base/consent/consent-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
isFirefoxDownloadThanks,
isURLExceptionAllowed,
isURLPermitted,
setConsentCookie
setConsentCookie,
setGtagAdsConsentMode,
setGtagAnalyticsConsentMode
} from '../../../../../media/js/base/consent/utils.es6';

describe('consentRequired()', function () {
Expand Down Expand Up @@ -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'
});
});
});
115 changes: 115 additions & 0 deletions tests/unit/spec/base/gtm/gtm-snippet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
});
});
});
Loading