Skip to content

Commit

Permalink
Add new link tracker
Browse files Browse the repository at this point in the history
- to allow tracking to be manually added to links on any page
  • Loading branch information
andysellick committed Nov 8, 2022
1 parent 2af4c50 commit 8f42c98
Show file tree
Hide file tree
Showing 3 changed files with 333 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//= require ./analytics-ga4/pii-remover
//= require ./analytics-ga4/ga4-page-views
//= require ./analytics-ga4/ga4-specialist-link-tracker
//= require ./analytics-ga4/ga4-link-tracker
//= require ./analytics-ga4/ga4-event-tracker
//= require ./analytics-ga4/ga4-ecommerce-tracker
//= require ./analytics-ga4/init-ga4
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//= require ../vendor/polyfills/closest.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

(function (Modules) {
'use strict'

function Ga4LinkTracker (module) {
this.module = module
this.trackingTrigger = 'data-ga4' // elements with this attribute get tracked
this.trackLinksOnly = this.module.hasAttribute('data-ga4-track-links-only')
this.limitToElementClass = this.module.getAttribute('data-ga4-limit-to-element-class')
}

Ga4LinkTracker.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
Ga4LinkTracker.prototype.startModule = function () {
if (window.dataLayer) {
this.module.addEventListener('click', function (event) {
var target = event.target
if (!this.trackLinksOnly) {
this.trackClick(event)
} else if (this.trackLinksOnly && target.closest('a')) {
if (!this.limitToElementClass) {
this.trackClick(event)
} else if (this.limitToElementClass && target.closest('.' + this.limitToElementClass)) {
this.trackClick(event)
}
}
}.bind(this), true) // useCapture must be true
}
}

Ga4LinkTracker.prototype.trackClick = function (event) {
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
}

schema.event = 'event_data'
data.text = event.target.textContent
data.url = this.findLink(event.target).getAttribute('href')
// 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]
}
}

window.GOVUK.analyticsGa4.core.sendData(schema)
}
}

Ga4LinkTracker.prototype.findLink = function (target) {
if (target.tagName === 'A') {
return target
} else {
return target.closest('a')
}
}

// FIXME duplicated from the event tracker - move to somewhere shared
Ga4LinkTracker.prototype.findTrackingAttributes = function (clicked) {
if (clicked.hasAttribute('[' + this.trackingTrigger + ']')) {
return clicked
} else {
return clicked.closest('[' + this.trackingTrigger + ']')
}
}

Modules.Ga4LinkTracker = Ga4LinkTracker
})(window.GOVUK.Modules)
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/* eslint-env jasmine */

describe('GA4 click tracker', function () {
var GOVUK = window.GOVUK
var element
var expected
var attributes

function initModule (element, click) {
new GOVUK.Modules.Ga4LinkTracker(element).init()
if (click) {
GOVUK.triggerEvent(element, 'click')
}
}

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 () {
spyOn(GOVUK.analyticsGa4.core, 'getGemVersion').and.returnValue('aVersion')
})

beforeEach(function () {
window.dataLayer = []
agreeToCookies()
})

afterEach(function () {
window.dataLayer = []
})

describe('when the user has a cookie consent choice', function () {
beforeEach(function () {
element = document.createElement('a')
})

it('starts the module if consent has already been given', function () {
agreeToCookies()
document.body.appendChild(element)
var tracker = new GOVUK.Modules.Ga4LinkTracker(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.Ga4LinkTracker(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.Ga4LinkTracker(element)
spyOn(tracker, 'trackClick')
tracker.init()

element.click()
expect(tracker.trackClick).not.toHaveBeenCalled()
})
})

describe('basic tracking', function () {
beforeEach(function () {
attributes = {
event_name: 'navigation',
type: 'a link'
}
expected = new GOVUK.analyticsGa4.Schemas().eventSchema()
expected.event = 'event_data'
expected.event_data.event_name = 'navigation'
expected.event_data.type = 'a link'
expected.govuk_gem_version = 'aVersion'
})

it('does not track anything without a data-ga4 attribute', function () {
element = document.createElement('a')
var linkText = 'Should not track'
element.textContent = linkText
expected.event_data.text = linkText

initModule(element, true)
expect(window.dataLayer[0]).toEqual(undefined)
})

it('tracks clicks on a single link', function () {
var link = '#aNormalLink'
element = document.createElement('a')
element.setAttribute('data-ga4', JSON.stringify(attributes))
element.setAttribute('href', link)
var linkText = 'A normal link'
element.textContent = linkText
expected.event_data.text = linkText
expected.event_data.url = link

initModule(element, true)
expect(window.dataLayer[0]).toEqual(expected)
})

it('tracks all links with data-ga4 attributes within a container', function () {
element = document.createElement('div')
element.innerHTML =
'<a class="first" href="#link1">Link 1</a>' +
'<a href="#link2" class="secondWrapper"><span class="second">Link 2</span></a>' +
'<a href="#link3"><span class="third">Link 3</span></a>' +
'<span class="nothing"></span>'

element.querySelector('.first').setAttribute('data-ga4', JSON.stringify(attributes))
element.querySelector('.secondWrapper').setAttribute('data-ga4', JSON.stringify(attributes))
initModule(element, false)

expected.event_data.text = 'Link 1'
expected.event_data.url = '#link1'
element.querySelector('.first').click()
expect(window.dataLayer[0]).toEqual(expected)

expected.event_data.text = 'Link 2'
expected.event_data.url = '#link2'
element.querySelector('.second').click()
expect(window.dataLayer[1]).toEqual(expected)

element.querySelector('.third').click()
expect(window.dataLayer[2]).toEqual(undefined)

element.querySelector('.nothing').click()
expect(window.dataLayer[2]).toEqual(undefined)
})
})

describe('when the track links only feature is enabled', function () {
beforeEach(function () {
attributes = {
event_name: 'navigation',
type: 'a link'
}
expected = new GOVUK.analyticsGa4.Schemas().eventSchema()
expected.event = 'event_data'
expected.event_data.event_name = 'navigation'
expected.event_data.type = 'a link'
expected.govuk_gem_version = 'aVersion'
})

it('tracks only links within a container', function () {
element = document.createElement('div')
element.innerHTML =
'<a class="first" href="#link1">Link 1</a>' +
'<a class="second" href="#link2">Link 2</a>' +
'<a href="#link3"><span class="third">Link 3</span></a>' +
'<span class="nothing"></span>'

element.setAttribute('data-ga4-track-links-only', '')
element.setAttribute('data-ga4', JSON.stringify(attributes))
initModule(element, false)

expected.event_data.text = 'Link 1'
expected.event_data.url = '#link1'
element.querySelector('.first').click()
expect(window.dataLayer[0]).toEqual(expected)

expected.event_data.text = 'Link 2'
expected.event_data.url = '#link2'
element.querySelector('.second').click()
expect(window.dataLayer[1]).toEqual(expected)

expected.event_data.text = 'Link 3'
expected.event_data.url = '#link3'
element.querySelector('.third').click()
expect(window.dataLayer[2]).toEqual(expected)

element.querySelector('.nothing').click()
expect(window.dataLayer[3]).toEqual(undefined)
})
})

describe('when the limit to element class feature is enabled', function () {
beforeEach(function () {
attributes = {
event_name: 'navigation',
type: 'a link'
}
expected = new GOVUK.analyticsGa4.Schemas().eventSchema()
expected.event = 'event_data'
expected.event_data.event_name = 'navigation'
expected.event_data.type = 'a link'
expected.govuk_gem_version = 'aVersion'
})

it('tracks only links within the given class', function () {
element = document.createElement('div')
element.innerHTML =
'<a class="first" href="#link1">Link 1</a>' +
'<div class="trackme">' +
'<a class="second" href="#link2">Link 2</a>' +
'<a href="#link3"><span class="third">Link 3</span></a>' +
'<span class="nothing"></span>' +
'</div>'

element.setAttribute('data-ga4-track-links-only', '')
element.setAttribute('data-ga4-limit-to-element-class', 'trackme')
element.setAttribute('data-ga4', JSON.stringify(attributes))
initModule(element, false)

expected.event_data.text = 'Link 1'
expected.event_data.url = '#link1'
element.querySelector('.first').click()
expect(window.dataLayer[0]).toEqual(undefined)

expected.event_data.text = 'Link 2'
expected.event_data.url = '#link2'
element.querySelector('.second').click()
expect(window.dataLayer[0]).toEqual(expected)

expected.event_data.text = 'Link 3'
expected.event_data.url = '#link3'
element.querySelector('.third').click()
expect(window.dataLayer[1]).toEqual(expected)

element.querySelector('.nothing').click()
expect(window.dataLayer[2]).toEqual(undefined)
})
})
})

0 comments on commit 8f42c98

Please sign in to comment.