Skip to content

Commit

Permalink
Merge pull request #3057 from alphagov/refactor-trackers
Browse files Browse the repository at this point in the history
Refactor GA4 analytics event and link trackers
  • Loading branch information
andysellick authored Nov 25, 2022
2 parents ae5c1b2 + ebdcfee commit 04225e5
Show file tree
Hide file tree
Showing 26 changed files with 1,810 additions and 1,229 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

## Unreleased

* **BREAKING** Refactor GA4 analytics event and link trackers ([PR #3057](https://github.com/alphagov/govuk_publishing_components/pull/3057))
* Share links allow data attributes ([PR #3072](https://github.com/alphagov/govuk_publishing_components/pull/3072))
* Update to LUX 304 ([PR #3070](https://github.com/alphagov/govuk_publishing_components/pull/3070))
* Set attributes for single page notification button based on Account API response ([PR #3071](https://github.com/alphagov/govuk_publishing_components/pull/3071))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//= require ./analytics-ga4/ga4-schemas
//= 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//= require ../vendor/polyfills/closest.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.analyticsGa4 = window.GOVUK.analyticsGa4 || {};

Expand Down Expand Up @@ -47,6 +48,180 @@ window.GOVUK.analyticsGa4 = window.GOVUK.analyticsGa4 || {};

getGemVersion: function () {
return window.GOVUK.analyticsGa4.vars.gem_version || 'not found'
},

trackFunctions: {
findTrackingAttributes: function (clicked, trackingTrigger) {
if (clicked.hasAttribute('[' + trackingTrigger + ']')) {
return clicked
} else {
return clicked.closest('[' + trackingTrigger + ']')
}
},

// create an object to split up long URLs and get around the 100 character limit on GTM data
// this gets reassembled in GA4
populateLinkPathParts: function (href) {
var path = ''
if (this.hrefIsRelative(href) || this.isMailToLink(href)) {
path = href
} else {
// This regex matches a protocol and domain name at the start of a string such as https://www.gov.uk, http://gov.uk, //gov.uk
path = href.replace(/^(http:||https:)?(\/\/)([^\/]*)/, '') // eslint-disable-line no-useless-escape
}

if (path === '/' || path.length === 0) {
return
}

/*
This will create an object with 5 keys that are indexes ("1", "2", etc.)
The values will be each part of the link path split every 100 characters, or undefined.
For example: {"1": "/hello/world/etc...", "2": "/more/path/text...", "3": undefined, "4": undefined, "5": undefined}
Undefined values are needed to override the persistent object in GTM so that any values from old pushes are overwritten.
*/
var parts = path.match(/.{1,100}/g)
var obj = {}
for (var i = 0; i < 5; i++) {
obj[(i + 1).toString()] = parts[i]
}
return obj
},

hrefIsRelative: function (href) {
// Checks that a link is relative, but is not a protocol relative url
return href[0] === '/' && href[1] !== '/'
},

hrefIsAnchor: function (href) {
return href[0] === '#'
},

isMailToLink: function (href) {
return href.substring(0, 7) === 'mailto:'
},

getClickType: function (event) {
switch (event.type) {
case 'click':
if (event.ctrlKey) {
return 'ctrl click'
} else if (event.metaKey) {
return 'command/win click'
} else if (event.shiftKey) {
return 'shift click'
} else {
return 'primary click'
}
case 'mousedown':
return 'middle click'
case 'contextmenu':
return 'secondary click'
}
},

isInternalLink: function (href) {
var internalDomains = window.GOVUK.analyticsGa4.vars.internalDomains
if (this.hrefIsRelative(href) || this.hrefIsAnchor(href)) {
return true
}
var result = false
for (var i = 0; i < internalDomains.length; i++) {
var internalDomain = internalDomains[i]
if (this.hrefPointsToDomain(href, internalDomain)) {
result = true
}
}
return result
},

isExternalLink: function (href) {
return !this.isInternalLink(href)
},

hrefPointsToDomain: function (href, domain) {
/* Add a trailing slash to prevent an edge case such
as the href www.gov.uk.domain.co.uk being detected as an internal link,
if we were checking for 'www.gov.uk' instead of 'www.gov.uk/' */
if (domain.substring(domain.length) !== '/') {
domain = domain + '/'
}

/* If the href doesn't end in a slash, we add one.
This fixes an edge case where the <a href> is exactly `https://www.gov.uk`
but these checks would only look for `https://www.gov.uk/` */
if (href.substring(href.length) !== '/') {
href = href + '/'
}
// matches the domain preceded by https:// http:// or //
var regex = new RegExp('^((http)*(s)*(:)*//)(' + domain + ')', 'g')
return regex.test(href)
},

removeLinesAndExtraSpaces: function (text) {
text = text.trim()
text = text.replace(/(\r\n|\n|\r)/gm, ' ') // Replace line breaks with 1 space
text = text.replace(/\s+/g, ' ') // Replace instances of 2+ spaces with 1 space
return text
},

removeCrossDomainParams: function (href) {
if (href.indexOf('_ga') !== -1 || href.indexOf('_gl') !== -1) {
// _ga & _gl are values needed for cross domain tracking, but we don't want them included in our click tracking.
href = href.replaceAll(/_g[al]=([^&]*)/g, '')

// The following code cleans up inconsistencies such as gov.uk/&&, gov.uk/?&hello=world, gov.uk/?, and gov.uk/&.
href = href.replaceAll(/(&&)+/g, '&')
href = href.replace('?&', '?')
if (this.stringEndsWith(href, '?') || this.stringEndsWith(href, '&')) {
href = href.substring(0, href.length - 1)
}
}
return href
},

stringStartsWith: function (string, stringToFind) {
return string.substring(0, stringToFind.length) === stringToFind
},

stringEndsWith: function (string, stringToFind) {
return string.substring(string.length - stringToFind.length, string.length) === stringToFind
},

populateLinkDomain: function (href) {
// We always want mailto links to have an undefined link_domain
if (this.isMailToLink(href)) {
return undefined
}

if (this.hrefIsRelative(href) || this.hrefIsAnchor(href)) {
return this.getProtocol() + '//' + this.getHostname()
} else {
// This regex matches a protocol and domain name at the start of a string such as https://www.gov.uk, http://gov.uk, //gov.uk
var domainRegex = /^(http:||https:)?(\/\/)([^\/]*)/ // eslint-disable-line no-useless-escape
var domain = domainRegex.exec(href)[0]
return domain
}
},

getProtocol: function () {
return window.location.protocol
},

getHostname: function () {
return window.location.hostname
},

appendDomainsWithoutWWW: function (domainsArrays) {
// Add domains with www. removed, in case site hrefs are marked up without www. included.
for (var i = 0; i < domainsArrays.length; i++) {
var domain = domainsArrays[i]
if (this.stringStartsWith(domain, 'www.')) {
var domainWithoutWww = domain.replace('www.', '')
domainsArrays.push(domainWithoutWww)
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
})

ecommerceObject.event_data = {
external: GOVUK.analyticsGa4.analyticsModules.Ga4LinkTracker.isExternalLink(searchResult.getAttribute('data-ecommerce-path')) ? 'true' : 'false'
external: GOVUK.analyticsGa4.core.trackFunctions.isExternalLink(searchResult.getAttribute('data-ecommerce-path')) ? 'true' : 'false'
}
} else {
for (var i = 0; i < ecommerceRows.length; i++) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// = require govuk/vendor/polyfills/Element/prototype/closest.js
//= require ../vendor/polyfills/closest.js
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};

Expand All @@ -7,7 +7,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};

function Ga4EventTracker (module) {
this.module = module
this.trackingTrigger = 'data-ga4' // elements with this attribute get tracked
this.trackingTrigger = 'data-ga4-event' // elements with this attribute get tracked
}

Ga4EventTracker.prototype.init = function () {
Expand All @@ -29,7 +29,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
}

Ga4EventTracker.prototype.trackClick = function (event) {
var target = this.findTrackingAttributes(event.target)
var target = window.GOVUK.analyticsGa4.core.trackFunctions.findTrackingAttributes(event.target, this.trackingTrigger)
if (target) {
var schema = new window.GOVUK.analyticsGa4.Schemas().eventSchema()

Expand Down Expand Up @@ -88,14 +88,6 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
}
}

Ga4EventTracker.prototype.findTrackingAttributes = function (clicked) {
if (clicked.hasAttribute('[' + this.trackingTrigger + ']')) {
return clicked
} else {
return clicked.closest('[' + this.trackingTrigger + ']')
}
}

// check if an attribute exists or contains the attribute
Ga4EventTracker.prototype.getClosestAttribute = function (clicked, attribute) {
var isAttributeOnElement = clicked.getAttribute(attribute)
Expand Down
Loading

0 comments on commit 04225e5

Please sign in to comment.