-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- to allow tracking to be manually added to links on any page
- Loading branch information
1 parent
2af4c50
commit 8f42c98
Showing
3 changed files
with
333 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
241 changes: 241 additions & 0 deletions
241
spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.spec.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) | ||
}) |