From 2109652f58d9d40987c2da60e4d4d9814746f44f Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Fri, 19 Aug 2022 15:26:44 +0100 Subject: [PATCH 1/7] Create a script to initialise GA4 - basic for now - will need to be called where the analytics scripts are imported, unless there's a better alternative - currently moves the basic stuff into a general init, but this will have to change as currently won't work correctly on the page where consent is given --- .../analytics-ga4.js | 4 +-- .../analytics-ga4/init-ga4.js | 19 +++++++++++++ .../analytics-ga4/init-ga4.spec.js | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js create mode 100644 spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js index 84b7fb72c8..b8f00f4bfa 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js @@ -4,6 +4,4 @@ //= require ./analytics-ga4/ga4-page-views //= require ./analytics-ga4/ga4-link-tracker //= require ./analytics-ga4/ga4-event-tracker - -window.GOVUK.analyticsGA4.pageViewTracker.sendPageView() // this will need integrating with cookie consent before production -window.GOVUK.analyticsGA4.linkTracker.trackLinkClicks() +//= require ./analytics-ga4/init-ga4 diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js new file mode 100644 index 0000000000..a2aa047826 --- /dev/null +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js @@ -0,0 +1,19 @@ +var analyticsGa4Init = function () { + // to be added: digital identity consent mechanism + + var consentCookie = window.GOVUK.getConsentCookie() + var dummyAnalytics = {} + + if (consentCookie && consentCookie.usage) { + window.GOVUK.analyticsGA4.pageViewTracker.sendPageView() + window.GOVUK.analyticsGA4.linkTracker.trackLinkClicks() + + // to be added: attach JS from Google to the DOM and execute + // to be added: cross domain tracking code + } else { + // clear the analytics object so no code can execute + window.GOVUK.analyticsGA4 = dummyAnalytics + } +} + +window.GOVUK.analyticsGa4Init = analyticsGa4Init diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js new file mode 100644 index 0000000000..613e989373 --- /dev/null +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js @@ -0,0 +1,28 @@ +/* eslint-env jasmine */ + +describe('Initialising GA4', function () { + var GOVUK = window.GOVUK + var save + + beforeEach(function () { + save = GOVUK.analyticsGA4 + }) + + afterEach(function () { + GOVUK.analyticsGA4 = save + }) + + it('creates the GA4 code when cookie consent is given', function () { + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + GOVUK.analyticsGa4Init() + + expect(GOVUK.analyticsGA4).not.toEqual({}) + }) + + it('clears the GA4 code when cookie consent is not given', function () { + GOVUK.setCookie('cookies_policy', '{"essential":false,"settings":false,"usage":false,"campaigns":false}') + GOVUK.analyticsGa4Init() + + expect(GOVUK.analyticsGA4).toEqual({}) + }) +}) From b2755a38f8c383e51f4df853bffa81bd28d810c6 Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Fri, 19 Aug 2022 15:28:43 +0100 Subject: [PATCH 2/7] Adjust event tracker - it was checking for the dataLayer at the point of creating the event listener, but should check before adding that --- .../analytics-ga4/ga4-event-tracker.js | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js index 89d95de1be..496db6a103 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js @@ -11,68 +11,68 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}; } Ga4EventTracker.prototype.init = function () { - this.module.addEventListener('click', this.trackClick.bind(this), true) // useCapture must be true + if (window.dataLayer) { + this.module.addEventListener('click', this.trackClick.bind(this), true) // useCapture must be true + } } Ga4EventTracker.prototype.trackClick = function (event) { - if (window.dataLayer) { - var target = this.findTrackingAttributes(event.target) - if (target) { - var schema = new window.GOVUK.analyticsGA4.Schemas().eventSchema() + var target = this.findTrackingAttributes(event.target) + if (target) { + var schema = new window.GOVUK.analyticsGA4.Schemas().eventSchema() - try { - var data = target.getAttribute(this.trackingTrigger) - data = JSON.parse(data) - } catch (e) { - // if there's a problem with the config, don't start the tracker - console.error('GA4 configuration error: ' + e.message, window.location) - return - } + try { + var data = target.getAttribute(this.trackingTrigger) + data = JSON.parse(data) + } catch (e) { + // if there's a problem with the config, don't start the tracker + console.error('GA4 configuration error: ' + e.message, window.location) + return + } - schema.event = 'event_data' - // get attributes from the data attribute to send to GA - // only allow it if it already exists in the schema - for (var property in data) { - if (property in schema.event_data) { - schema.event_data[property] = data[property] - } + schema.event = 'event_data' + // get attributes from the data attribute to send to GA + // only allow it if it already exists in the schema + for (var property in data) { + if (property in schema.event_data) { + schema.event_data[property] = data[property] } + } - // Ensure it only tracks aria-expanded in an accordion element, instead of in any child of the clicked element - if (target.closest('.gem-c-accordion')) { - var ariaExpanded = this.getClosestAttribute(target, 'aria-expanded') - } + // Ensure it only tracks aria-expanded in an accordion element, instead of in any child of the clicked element + if (target.closest('.gem-c-accordion')) { + var ariaExpanded = this.getClosestAttribute(target, 'aria-expanded') + } - /* - the details component uses an 'open' attribute instead of aria-expanded, so we need to check if we're on a details component. - since details deletes the 'open' attribute when closed, we need this boolean, otherwise every element which - doesn't contain an 'open' attr would be pushed to gtm as a closed element. - */ - var detailsElement = target.closest('details') + /* + the details component uses an 'open' attribute instead of aria-expanded, so we need to check if we're on a details component. + since details deletes the 'open' attribute when closed, we need this boolean, otherwise every element which + doesn't contain an 'open' attr would be pushed to gtm as a closed element. + */ + var detailsElement = target.closest('details') - if (ariaExpanded) { - schema.event_data.text = data.text || target.innerText - schema.event_data.action = (ariaExpanded === 'false') ? 'opened' : 'closed' - } else if (detailsElement) { - schema.event_data.text = data.text || detailsElement.textContent - var openAttribute = detailsElement.getAttribute('open') - schema.event_data.action = (openAttribute == null) ? 'opened' : 'closed' - } + if (ariaExpanded) { + schema.event_data.text = data.text || target.innerText + schema.event_data.action = (ariaExpanded === 'false') ? 'opened' : 'closed' + } else if (detailsElement) { + schema.event_data.text = data.text || detailsElement.textContent + var openAttribute = detailsElement.getAttribute('open') + schema.event_data.action = (openAttribute == null) ? 'opened' : 'closed' + } - /* If a tab was clicked, grab the href of the clicked tab (usually an anchor # link) */ - var tabElement = event.target.closest('.gem-c-tabs') - if (tabElement) { - var aTag = event.target.closest('a') - if (aTag) { - var href = aTag.getAttribute('href') - if (href) { - schema.event_data.url = href - } + /* If a tab was clicked, grab the href of the clicked tab (usually an anchor # link) */ + var tabElement = event.target.closest('.gem-c-tabs') + if (tabElement) { + var aTag = event.target.closest('a') + if (aTag) { + var href = aTag.getAttribute('href') + if (href) { + schema.event_data.url = href } } - - window.dataLayer.push(schema) } + + window.dataLayer.push(schema) } } From d39995faffa20bdaf963561fdff75a7a585e0f7b Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Fri, 19 Aug 2022 16:19:58 +0100 Subject: [PATCH 3/7] Implement code initialisation for GA4 on consent - trying to build a model where we don't run any analytics code unless the user consents - have converted ga4-link-tracker as a first attempt at this, the rest to follow - GA4 analytics code will fit into the window.GOVUK.analyticsGA4.analyticsModules object - on consent, the code will iterate through the objects in that object and look for an init function, executing it if found - the init function will determine what happens in each analytics module e.g. set up event listeners, fire a page view --- .../analytics-ga4/ga4-link-tracker.js | 16 ++-- .../analytics-ga4/init-ga4.js | 23 +++-- .../analytics-ga4/ga4-link-tracker.spec.js | 12 +-- .../analytics-ga4/init-ga4.spec.js | 91 ++++++++++++++++--- 4 files changed, 109 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js index 381545ce52..a48f531e58 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js @@ -1,13 +1,13 @@ // = require govuk/vendor/polyfills/Element/prototype/closest.js +window.GOVUK = window.GOVUK || {} +window.GOVUK.analyticsGA4 = window.GOVUK.analyticsGA4 || {} +window.GOVUK.analyticsGA4.analyticsModules = window.GOVUK.analyticsGA4.analyticsModules || {}; -;(function (global) { +(function (analyticsModules) { 'use strict' - var GOVUK = global.GOVUK || {} - GOVUK.analyticsGA4 = GOVUK.analyticsGA4 || {} - - GOVUK.analyticsGA4.linkTracker = { - trackLinkClicks: function () { + var Ga4LinkTracker = { + init: function () { if (window.dataLayer) { this.internalLinksDomain = 'www.gov.uk/' this.internalLinksDomainWithoutWww = 'gov.uk/' @@ -167,5 +167,5 @@ } } - global.GOVUK = GOVUK -})(window) + analyticsModules.Ga4LinkTracker = Ga4LinkTracker +})(window.GOVUK.analyticsGA4.analyticsModules) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js index a2aa047826..f919f81336 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js @@ -1,19 +1,26 @@ -var analyticsGa4Init = function () { +window.GOVUK = window.GOVUK || {} +window.GOVUK.analyticsGA4 = window.GOVUK.analyticsGA4 || {} + +var initFunction = function () { // to be added: digital identity consent mechanism var consentCookie = window.GOVUK.getConsentCookie() - var dummyAnalytics = {} if (consentCookie && consentCookie.usage) { - window.GOVUK.analyticsGA4.pageViewTracker.sendPageView() - window.GOVUK.analyticsGA4.linkTracker.trackLinkClicks() - + var analyticsModules = window.GOVUK.analyticsGA4.analyticsModules + for (var property in analyticsModules) { + var module = analyticsModules[property] + if (typeof module.init === 'function') { + module.init() + } + } // to be added: attach JS from Google to the DOM and execute // to be added: cross domain tracking code } else { - // clear the analytics object so no code can execute - window.GOVUK.analyticsGA4 = dummyAnalytics + window.addEventListener('cookie-consent', function () { + window.GOVUK.analyticsGA4.init() + }) } } -window.GOVUK.analyticsGa4Init = analyticsGa4Init +window.GOVUK.analyticsGA4.init = initFunction diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.spec.js index 2b20fadb1c..481fde4795 100644 --- a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.spec.js +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.spec.js @@ -61,8 +61,8 @@ describe('GOVUK.analyticsGA4.linkTracker', function () { body.appendChild(links) body.addEventListener('click', preventDefault) - linkTracker = GOVUK.analyticsGA4.linkTracker - linkTracker.trackLinkClicks() + linkTracker = GOVUK.analyticsGA4.analyticsModules.Ga4LinkTracker + linkTracker.init() }) afterEach(function () { @@ -291,8 +291,8 @@ describe('GOVUK.analyticsGA4.linkTracker', function () { body.appendChild(links) body.addEventListener('click', preventDefault) - linkTracker = GOVUK.analyticsGA4.linkTracker - linkTracker.trackLinkClicks() + linkTracker = GOVUK.analyticsGA4.analyticsModules.Ga4LinkTracker + linkTracker.init() }) afterEach(function () { @@ -463,8 +463,8 @@ describe('GOVUK.analyticsGA4.linkTracker', function () { body.appendChild(links) body.addEventListener('click', preventDefault) - linkTracker = GOVUK.analyticsGA4.linkTracker - linkTracker.trackLinkClicks() + linkTracker = GOVUK.analyticsGA4.analyticsModules.Ga4LinkTracker + linkTracker.init() }) afterEach(function () { diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js index 613e989373..1b52f0f135 100644 --- a/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.spec.js @@ -2,27 +2,96 @@ describe('Initialising GA4', function () { var GOVUK = window.GOVUK - var save + var analyticsGA4Save beforeEach(function () { - save = GOVUK.analyticsGA4 + analyticsGA4Save = GOVUK.analyticsGA4 }) afterEach(function () { - GOVUK.analyticsGA4 = save + GOVUK.analyticsGA4 = analyticsGA4Save }) - it('creates the GA4 code when cookie consent is given', function () { - GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') - GOVUK.analyticsGa4Init() + describe('when consent is given', function () { + var test = { + functionThatMightBeCalled: function () {} + } - expect(GOVUK.analyticsGA4).not.toEqual({}) + beforeEach(function () { + spyOn(test, 'functionThatMightBeCalled') + GOVUK.analyticsGA4.analyticsModules.Test = function () {} + GOVUK.analyticsGA4.analyticsModules.Test.init = function () { test.functionThatMightBeCalled() } + }) + + it('calls analytics modules successfully', function () { + spyOn(GOVUK.analyticsGA4.analyticsModules.Test, 'init').and.callThrough() + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + GOVUK.analyticsGA4.init() + + expect(test.functionThatMightBeCalled).toHaveBeenCalled() + }) + + it('does not call analytics modules without a valid init function', function () { + GOVUK.analyticsGA4.analyticsModules.Test.init = false + spyOn(GOVUK.analyticsGA4.analyticsModules.Test, 'init').and.callThrough() + + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + GOVUK.analyticsGA4.init() + + expect(test.functionThatMightBeCalled).not.toHaveBeenCalled() + }) + + it('does not error if no init is found at all', function () { + GOVUK.analyticsGA4.analyticsModules.Test = false + + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + GOVUK.analyticsGA4.init() + + expect(GOVUK.analyticsGA4).not.toEqual({}) + }) }) - it('clears the GA4 code when cookie consent is not given', function () { - GOVUK.setCookie('cookies_policy', '{"essential":false,"settings":false,"usage":false,"campaigns":false}') - GOVUK.analyticsGa4Init() + describe('Modules depending on cookie consent to run', function () { + var testModule + var testObject = { + testFunction: function () {} + } + + beforeEach(function () { + function TestModule () {} + + TestModule.prototype.init = function () { + var consentCookie = window.GOVUK.getConsentCookie() + + if (consentCookie && consentCookie.settings) { + this.startModule() + } else { + this.startModule = this.startModule.bind(this) + window.addEventListener('cookie-consent', this.startModule) + } + } + TestModule.prototype.startModule = function () { + testObject.testFunction() + } + + testModule = new TestModule() + spyOn(testObject, 'testFunction') + }) + + it('do not run if consent is not given', function () { + GOVUK.setCookie('cookies_policy', '{"essential":false,"settings":false,"usage":false,"campaigns":false}') + GOVUK.analyticsGA4.init() + + testModule.init() + expect(testObject.testFunction).not.toHaveBeenCalled() + }) + + it('run if consent is given', function () { + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + GOVUK.analyticsGA4.init() - expect(GOVUK.analyticsGA4).toEqual({}) + testModule.init() + expect(testObject.testFunction).toHaveBeenCalled() + }) }) }) From c2ab4bf018fe76a7b8b742c0d09396381d53b2f9 Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Tue, 6 Sep 2022 15:07:56 +0100 Subject: [PATCH 4/7] Convert page views to new format - update page views code to use the new format for GA4 code initialisation --- .../analytics-ga4/ga4-page-views.js | 21 +++++----- .../analytics-ga4/ga4-page-views.spec.js | 38 +++++++++---------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js index 4e9a330171..f73f394609 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js @@ -1,14 +1,15 @@ -;(function (global) { - 'use strict' +window.GOVUK = window.GOVUK || {} +window.GOVUK.analyticsGA4 = window.GOVUK.analyticsGA4 || {} +window.GOVUK.analyticsGA4.analyticsModules = window.GOVUK.analyticsGA4.analyticsModules || {}; - var GOVUK = global.GOVUK || {} - GOVUK.analyticsGA4 = GOVUK.analyticsGA4 || {} +(function (analyticsModules) { + 'use strict' - GOVUK.analyticsGA4.pageViewTracker = { - PIIRemover: new GOVUK.analyticsGA4.PIIRemover(), // imported in analytics-ga4.js + var PageViewTracker = { + PIIRemover: new window.GOVUK.analyticsGA4.PIIRemover(), // imported in analytics-ga4.js nullValue: null, - sendPageView: function () { + init: function () { if (window.dataLayer) { var data = { event: 'page_view', @@ -61,7 +62,7 @@ }, // window.httpStatusCode is set in the source of the error page in static - // https://github.com/alphagov/static/blob/main/app/views/root/_error_page.html.erb#L32 + // https://github.com/alphagov/static/blob/1c734451f2dd6fc0c7e80beccbdcbfa5aaffd0e4/app/views/root/_error_page.html.erb#L41-L43 getStatusCode: function () { if (window.httpStatusCode) { return window.httpStatusCode.toString() @@ -109,5 +110,5 @@ } } - global.GOVUK = GOVUK -})(window) + analyticsModules.PageViewTracker = PageViewTracker +})(window.GOVUK.analyticsGA4.analyticsModules) diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.spec.js index 2b0d45075c..3c3aa9b590 100644 --- a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.spec.js +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.spec.js @@ -66,14 +66,14 @@ describe('Google Tag Manager page view tracking', function () { } it('returns a standard page view', function () { - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a page view with a specific status code', function () { window.httpStatusCode = 404 expected.page_view.status_code = '404' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) @@ -112,7 +112,7 @@ describe('Google Tag Manager page view tracking', function () { expected.page_view[tag.gtmName] = tag.value } - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) @@ -156,7 +156,7 @@ describe('Google Tag Manager page view tracking', function () { expected.page_view[tag.gtmName] = tag.value } - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) @@ -177,14 +177,14 @@ describe('Google Tag Manager page view tracking', function () { content.setAttribute('lang', 'wakandan') expected.page_view.language = 'wakandan' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('set incorrectly', function () { expected.page_view.language = nullValue - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) }) @@ -192,84 +192,84 @@ describe('Google Tag Manager page view tracking', function () { it('returns a pageview without a language', function () { expected.page_view.language = nullValue - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview with history', function () { createMetaTags('content-has-history', 'true') expected.page_view.history = 'true' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview without history', function () { createMetaTags('content-has-history', 'banana') expected.page_view.history = 'false' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a withdrawn page', function () { createMetaTags('withdrawn', 'withdrawn') expected.page_view.withdrawn = 'true' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page with a first published date', function () { createMetaTags('first-published-at', '2022-03-28T19:11:00.000+00:00') expected.page_view.first_published_at = '2022-03-28' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page with a last updated date', function () { createMetaTags('updated-at', '2021-03-28T19:11:00.000+00:00') expected.page_view.updated_at = '2021-03-28' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page with a last public updated date', function () { createMetaTags('public-updated-at', '2020-03-28T19:11:00.000+00:00') expected.page_view.public_updated_at = '2020-03-28' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page marked with a publishing government', function () { createMetaTags('publishing-government', 'labour') expected.page_view.publishing_government = 'labour' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page marked with a political status', function () { createMetaTags('political-status', 'ongoing') expected.page_view.political_status = 'ongoing' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page marked with a primary publishing organisation', function () { createMetaTags('primary-publishing-organisation', 'Home Office') expected.page_view.primary_publishing_organisation = 'Home Office' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page marked with ids for contributing organisations', function () { createMetaTags('analytics:organisations', 'some organisations') expected.page_view.organisations = 'some organisations' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) it('returns a pageview on a page marked with world locations', function () { createMetaTags('analytics:world-locations', 'some world locations') expected.page_view.world_locations = 'some world locations' - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() expect(window.dataLayer[0]).toEqual(expected) }) @@ -294,7 +294,7 @@ describe('Google Tag Manager page view tracking', function () { linkForURLMock.href = '#example@gov.uk' linkForURLMock.click() - GOVUK.analyticsGA4.pageViewTracker.sendPageView() + GOVUK.analyticsGA4.analyticsModules.PageViewTracker.init() // Reset the page location for other tests linkForURLMock.href = '#' From 49711a34d756b62d0c6b3f043f5b7889d6532218 Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Tue, 6 Sep 2022 15:09:19 +0100 Subject: [PATCH 5/7] Use cookie consent in event tracker - update module to have a delayed start if consent has not been given - if consent is given, the cookie-consent event fires and the module is immediately started (no need for page reload) --- .../analytics-ga4/ga4-event-tracker.js | 12 +++++ .../analytics-ga4/ga4-event-tracker.spec.js | 50 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js index 496db6a103..b600a76b28 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js @@ -11,6 +11,18 @@ window.GOVUK.Modules = window.GOVUK.Modules || {}; } Ga4EventTracker.prototype.init = function () { + var consentCookie = window.GOVUK.getConsentCookie() + + if (consentCookie && consentCookie.settings) { + this.startModule() + } else { + this.startModule = this.startModule.bind(this) + window.addEventListener('cookie-consent', this.startModule) + } + } + + // triggered by cookie-consent event, which happens when users consent to cookies + Ga4EventTracker.prototype.startModule = function () { if (window.dataLayer) { this.module.addEventListener('click', this.trackClick.bind(this), true) // useCapture must be true } diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.spec.js index 3b9b37685b..094a47696e 100644 --- a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.spec.js +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.spec.js @@ -5,15 +5,65 @@ describe('Google Analytics event tracking', function () { var element var expected + function agreeToCookies () { + GOVUK.setCookie('cookies_policy', '{"essential":true,"settings":true,"usage":true,"campaigns":true}') + } + + function denyCookies () { + GOVUK.setCookie('cookies_policy', '{"essential":false,"settings":false,"usage":false,"campaigns":false}') + } + beforeEach(function () { window.dataLayer = [] element = document.createElement('div') + agreeToCookies() }) afterEach(function () { document.body.removeChild(element) }) + describe('when the user has a cookie consent choice', function () { + it('starts the module if consent has already been given', function () { + agreeToCookies() + document.body.appendChild(element) + var tracker = new GOVUK.Modules.Ga4EventTracker(element) + spyOn(tracker, 'trackClick') + tracker.init() + + element.click() + expect(tracker.trackClick).toHaveBeenCalled() + }) + + it('starts the module on the same page as cookie consent is given', function () { + denyCookies() + document.body.appendChild(element) + var tracker = new GOVUK.Modules.Ga4EventTracker(element) + spyOn(tracker, 'trackClick') + tracker.init() + + element.click() + expect(tracker.trackClick).not.toHaveBeenCalled() + + // page has not been reloaded, user consents to cookies + window.GOVUK.triggerEvent(window, 'cookie-consent') + + element.click() + expect(tracker.trackClick).toHaveBeenCalled() + }) + + it('does not do anything if consent is not given', function () { + denyCookies() + document.body.appendChild(element) + var tracker = new GOVUK.Modules.Ga4EventTracker(element) + spyOn(tracker, 'trackClick') + tracker.init() + + element.click() + expect(tracker.trackClick).not.toHaveBeenCalled() + }) + }) + describe('configuring tracking without any data', function () { beforeEach(function () { element.setAttribute('data-ga4', '') From 75ad14c3863104aef690f591548892cd155b0b37 Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Wed, 7 Sep 2022 16:56:16 +0100 Subject: [PATCH 6/7] Write documentation - document new analytics init, code structure for analytics modules, integration of new analytics into existing Modules --- docs/analytics-gtm/analytics.md | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/docs/analytics-gtm/analytics.md b/docs/analytics-gtm/analytics.md index 7555f26429..bb8b0b1a10 100644 --- a/docs/analytics-gtm/analytics.md +++ b/docs/analytics-gtm/analytics.md @@ -18,6 +18,42 @@ Events happen when a user interacts with certain things, for example clicking on Search data is gathered when users perform a search. +## Cookie consent + +The analytics code is only loaded if users consent to cookies. This is managed by the `init-ga4.js` script. + +If the page loads and cookie consent has already been given, the analytics code is initialised. This includes sending a page view and creating any event listeners for analytics code such as link tracking. + +If the page loads and cookie consent has not been given, an event listener is created for the `cookie-consent` event, which is dispatched by the [cookie banner component](https://github.com/alphagov/govuk_publishing_components/pull/2041/commits/777a381d2ccb67f0a7e78ebf659be806d8d6442d). If triggered, the event listener will initialise the analytics code as described above. This allows analytics to begin on the page where the user consents to cookies. + +## Code structure + +It is important that no analytics code runs until cookie consent is given. Code to be initialised as part of cookie consent should be attached to the `window.GOVUK.analyticsGA4.analyticsModules` object and include an `init` function, using the structure shown below. + +```JavaScript +window.GOVUK = window.GOVUK || {} +window.GOVUK.analyticsGA4 = window.GOVUK.analyticsGA4 || {} +window.GOVUK.analyticsGA4.analyticsModules = window.GOVUK.analyticsGA4.analyticsModules || {}; + +(function (analyticsModules) { + 'use strict' + + var ExampleCode = { + init: function () { + // do analytics stuff, like send a page view + } + } + + analyticsModules.ExampleCode = ExampleCode +})(window.GOVUK.analyticsGA4.analyticsModules) +``` + +When cookie consent is given, `init-ga4.js` looks through the `analyticsModules` object for anything with an `init` function, and executes them if found. This means that analytics code will not be executed unless consent is given, and gives a standard way to add more analytics code without additional initialisation. + +### Code structure for Modules + +Where analytics code is required as a [GOV.UK JavaScript Module](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/javascript-modules.md), the code structure for the [existing model for deferred loading](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/javascript-modules.md#modules-and-cookie-consent) should be used. + ## Data schemas All of the data sent to GTM is based on a common schema. From e36947be48169b92431743721850e9bc8fa10588 Mon Sep 17 00:00:00 2001 From: Andy Sellick Date: Wed, 7 Sep 2022 17:02:24 +0100 Subject: [PATCH 7/7] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b964612fa..920f9b4040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ useful summary for people upgrading their application, not a replication of the commit log. +## Unreleased + +* Integrate GA4 analytics code with cookie consent mechanism ([PR #2915](https://github.com/alphagov/govuk_publishing_components/pull/2915)) + ## 30.4.1 * Revert addition of `awesome_print` gem ([PR #2943](https://github.com/alphagov/govuk_publishing_components/pull/2943))