Skip to content

Commit

Permalink
Merge pull request #3920 from alphagov/ga4-focus-loss-tracker
Browse files Browse the repository at this point in the history
Add a new GA4 'focus loss' tracker
  • Loading branch information
AshGDS committed Mar 15, 2024
2 parents 494c9fd + c4cad6f commit b7651f7
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
Expand Up @@ -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
@@ -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)
4 changes: 4 additions & 0 deletions docs/analytics-ga4/ga4-all-trackers.md
Expand Up @@ -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.
Expand Down
16 changes: 16 additions & 0 deletions 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
<div
data-module="ga4-focus-loss-tracker"
data-ga4-focus-loss='{ "event_name": "filter", "type": "filter" }'>
</div>
```

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.
@@ -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)
})
})
})

0 comments on commit b7651f7

Please sign in to comment.