From c4cad6ff6fcc15409275666185366fb7d76b2d89 Mon Sep 17 00:00:00 2001 From: AshGDS <8880610+AshGDS@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:30:22 +0000 Subject: [PATCH] Add a new GA4 'focus loss' tracker --- CHANGELOG.md | 4 + .../analytics-ga4.js | 1 + .../analytics-ga4/ga4-focus-loss-tracker.js | 61 +++++ docs/analytics-ga4/ga4-all-trackers.md | 4 + docs/analytics-ga4/ga4-focus-loss-tracker.md | 16 ++ .../ga4-focus-loss-tracker.spec.js | 233 ++++++++++++++++++ 6 files changed, 319 insertions(+) create mode 100644 app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.js create mode 100644 docs/analytics-ga4/ga4-focus-loss-tracker.md create mode 100644 spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index df59ffe63a..7ed84c4e11 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 + +* Add a new GA4 'focus loss' tracker ([PR #3920](https://github.com/alphagov/govuk_publishing_components/pull/3920)) + ## 37.8.1 * Remove "Popular" links from super navigation header ([PR #3918](https://github.com/alphagov/govuk_publishing_components/pull/3918)) diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js index 55484a2d3e..5399bb9c42 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js @@ -14,4 +14,5 @@ //= require ./analytics-ga4/ga4-smart-answer-results-tracker //= require ./analytics-ga4/ga4-scroll-tracker //= require ./analytics-ga4/ga4-video-tracker +//= require ./analytics-ga4/ga4-focus-loss-tracker //= require ./analytics-ga4/init-ga4 diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.js new file mode 100644 index 0000000000..990282a9bc --- /dev/null +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.js @@ -0,0 +1,61 @@ +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + 'use strict' + + function Ga4FocusLossTracker (module) { + this.module = module + this.trackingTrigger = 'data-ga4-focus-loss' // elements with this attribute get tracked + } + + Ga4FocusLossTracker.prototype.init = function () { + var consentCookie = window.GOVUK.getConsentCookie() + + if (consentCookie && consentCookie.usage) { + this.startModule() + } else { + this.start = this.startModule.bind(this) + window.addEventListener('cookie-consent', this.start) + } + } + + // triggered by cookie-consent event, which happens when users consent to cookies + Ga4FocusLossTracker.prototype.startModule = function () { + if (window.dataLayer) { + window.removeEventListener('cookie-consent', this.start) + this.module.addEventListener('blur', this.trackFocusLoss.bind(this)) + this.module.piiRemover = new window.GOVUK.analyticsGa4.PIIRemover() + } + } + + Ga4FocusLossTracker.prototype.trackFocusLoss = function (event) { + var data = event.target.getAttribute(this.trackingTrigger) + if (!data) { + return + } + + try { + data = JSON.parse(data) + } catch (e) { + // if there's a problem with the config, don't start the tracker + console.warn('GA4 configuration error: ' + e.message, window.location) + return + } + + var tagName = event.target.tagName + var inputType = event.target.getAttribute('type') + + if (data.text) { + data.text = this.module.piiRemover.stripPIIWithOverride(data.text, true, true) + } else { + if (tagName === 'INPUT' && (inputType === 'search' || inputType === 'text')) { + data.text = window.GOVUK.analyticsGa4.core.trackFunctions.standardiseSearchTerm(this.module.value) + } + } + + window.GOVUK.analyticsGa4.core.applySchemaAndSendData(data, 'event_data') + } + + Modules.Ga4FocusLossTracker = Ga4FocusLossTracker +})(window.GOVUK.Modules) diff --git a/docs/analytics-ga4/ga4-all-trackers.md b/docs/analytics-ga4/ga4-all-trackers.md index 2cf1b54e7b..10fbcd8665 100644 --- a/docs/analytics-ga4/ga4-all-trackers.md +++ b/docs/analytics-ga4/ga4-all-trackers.md @@ -20,6 +20,10 @@ The [ecommerce tracker](https://github.com/alphagov/govuk_publishing_components/ The [event tracker](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/analytics-ga4/ga4-event-tracker.md) handles tracking on buttons or other interactive elements, such as tabs and details elements. +## Focus loss tracker + +The [focus loss tracker](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/analytics-ga4/ga4-focus-loss-tracker.md) is designed to capture data about an element when it loses focus. + ## Form tracker The [form tracker](https://github.com/alphagov/govuk_publishing_components/blob/main/docs/analytics-ga4/ga4-form-tracker.md) is designed to capture data about a form when it has been submitted. diff --git a/docs/analytics-ga4/ga4-focus-loss-tracker.md b/docs/analytics-ga4/ga4-focus-loss-tracker.md new file mode 100644 index 0000000000..a2a99dfaf1 --- /dev/null +++ b/docs/analytics-ga4/ga4-focus-loss-tracker.md @@ -0,0 +1,16 @@ +# Google Analytics 4 focus loss tracker + +This script is intended for adding GA4 tracking to any element that needs to be tracked when focus is removed from it. + +## Basic use + +```html +
+
+``` + +If the tracker is initialised on an input that has the type `text` or `search`, the tracker will automatically grab the input value and set it as the `text` value in the GA4 JSON. This can be overridden by simply adding your own text value in `data-ga4-focus-loss`. + +This module was created to record what users were searching for in a client side DOM search filter. We did not want to track each keypress the user made as that would spam our analytics data. Therefore by tracking when focus is lost on the search input, we can see what keyword was leading to a user navigating off of the search box and onto the result they wanted. \ No newline at end of file diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.spec.js new file mode 100644 index 0000000000..64ee4d9f4f --- /dev/null +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-focus-loss-tracker.spec.js @@ -0,0 +1,233 @@ +/* eslint-env jasmine */ + +describe('GA4 focus loss tracker', function () { + var GOVUK = window.GOVUK + 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}') + } + + beforeAll(function () { + window.GOVUK.analyticsGa4 = window.GOVUK.analyticsGa4 || {} + window.GOVUK.analyticsGa4.vars = window.GOVUK.analyticsGa4.vars || {} + window.GOVUK.analyticsGa4.vars.gem_version = 'aVersion' + }) + + beforeEach(function () { + window.dataLayer = [] + element = document.createElement('div') + document.body.appendChild(element) + agreeToCookies() + spyOn(GOVUK.analyticsGa4.core, 'getTimestamp').and.returnValue('123456') + }) + + afterEach(function () { + document.body.removeChild(element) + }) + + afterAll(function () { + GOVUK.setCookie('cookies_policy', null) + window.dataLayer = [] + }) + + describe('when the user has a cookie consent choice', function () { + it('starts the module if consent has already been given', function () { + agreeToCookies() + var tracker = new GOVUK.Modules.Ga4FocusLossTracker(element) + spyOn(tracker, 'startModule').and.callThrough() + tracker.init() + + expect(tracker.startModule).toHaveBeenCalled() + }) + + it('starts the module on the same page as cookie consent is given', function () { + denyCookies() + var tracker = new GOVUK.Modules.Ga4FocusLossTracker(element) + spyOn(tracker, 'startModule').and.callThrough() + tracker.init() + expect(tracker.startModule).not.toHaveBeenCalled() + + // page has not been reloaded, user consents to cookies + window.GOVUK.triggerEvent(window, 'cookie-consent') + expect(tracker.startModule).toHaveBeenCalled() + + // consent listener should be removed after triggering + tracker.startModule.calls.reset() + window.GOVUK.triggerEvent(window, 'cookie-consent') + expect(tracker.startModule).not.toHaveBeenCalled() + }) + + it('does not do anything if consent is not given', function () { + denyCookies() + var tracker = new GOVUK.Modules.Ga4FocusLossTracker(element) + spyOn(tracker, 'startModule') + tracker.init() + + expect(tracker.startModule).not.toHaveBeenCalled() + }) + }) + + describe('configuring tracking without any data', function () { + beforeEach(function () { + element.setAttribute('data-ga4-focus-loss', '') + new GOVUK.Modules.Ga4FocusLossTracker(element).init() + }) + + it('does not cause an error or fire an event', function () { + expect(window.dataLayer[0]).toEqual(undefined) + }) + }) + + describe('configuring tracking with incorrect data', function () { + beforeEach(function () { + element.setAttribute('data-ga4-focus-loss', 'invalid json') + new GOVUK.Modules.Ga4FocusLossTracker(element).init() + }) + + it('does not cause an error', function () { + expect(window.dataLayer[0]).toEqual(undefined) + }) + }) + + describe('tracking on focus loss', function () { + beforeEach(function () { + expected = new GOVUK.analyticsGa4.Schemas().eventSchema() + expected.event = 'event_data' + expected.event_data.event_name = 'filter' + expected.event_data.type = 'filter' + expected.govuk_gem_version = 'aVersion' + expected.timestamp = '123456' + + var attributes = { + event_name: 'filter', + type: 'filter', + not_a_schema_attribute: 'something' + } + element.setAttribute('data-ga4-focus-loss', JSON.stringify(attributes)) + new GOVUK.Modules.Ga4FocusLossTracker(element).init() + }) + + it('pushes ga4 attributes to the dataLayer when the element is focussed on, and then focus changes', function () { + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + expect(window.dataLayer[0].not_a_schema_attribute).toEqual(undefined) + }) + + it('doesnt do anything when focus is moved around within the element', function () { + var child1 = document.createElement('a') + var child2 = document.createElement('a') + element.appendChild(child1) + element.appendChild(child2) + + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(child1, 'focus') + window.GOVUK.triggerEvent(child1, 'blur') + window.GOVUK.triggerEvent(child2, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + }) + }) + + describe('automatically grabs text or search input type values', function () { + beforeEach(function () { + window.dataLayer = [] + expected = new GOVUK.analyticsGa4.Schemas().eventSchema() + expected.event = 'event_data' + expected.event_data.event_name = 'filter' + expected.event_data.type = 'filter' + expected.govuk_gem_version = 'aVersion' + expected.timestamp = '123456' + + var attributes = { + event_name: 'filter', + type: 'filter', + not_a_schema_attribute: 'something' + } + document.body.removeChild(element) + element = document.createElement('input') + element.setAttribute('data-ga4-focus-loss', JSON.stringify(attributes)) + document.body.appendChild(element) + + new GOVUK.Modules.Ga4FocusLossTracker(element).init() + }) + + it('pushes the current text input to the dataLayer', function () { + element.setAttribute('type', 'text') + element.value = 'green tea' + expected.event_data.text = 'green tea' + + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + }) + + it('pushes the current search input to the dataLayer', function () { + element.setAttribute('type', 'search') + element.value = 'black tea' + expected.event_data.text = 'black tea' + + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + }) + + it('should remove extra spaces from the input', function () { + element.setAttribute('type', 'search') + element.value = ' black tea ' + expected.event_data.text = 'black tea' + + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + }) + + it('should set the input text to lowercase', function () { + element.setAttribute('type', 'search') + element.value = 'BLACK TEA' + expected.event_data.text = 'black tea' + + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + }) + }) + + describe('PII removal', function () { + beforeEach(function () { + expected = new GOVUK.analyticsGa4.Schemas().eventSchema() + expected.event = 'event_data' + expected.event_data.event_name = 'filter' + expected.event_data.type = 'filter' + expected.event_data.text = '/[date]/[postcode]/[email]' + expected.govuk_gem_version = 'aVersion' + expected.timestamp = '123456' + + var attributes = { + event_name: 'filter', + type: 'filter', + text: '/2022-02-02/SW10AA/email@example.com' + } + element.setAttribute('data-ga4-focus-loss', JSON.stringify(attributes)) + new GOVUK.Modules.Ga4FocusLossTracker(element).init() + }) + + it('redacts dates, postcodes and emails from text', function () { + window.GOVUK.triggerEvent(element, 'focus') + expect(window.dataLayer[0]).toEqual(undefined) + window.GOVUK.triggerEvent(element, 'blur') + expect(window.dataLayer[0]).toEqual(expected) + }) + }) +})