Skip to content

Commit

Permalink
Adjust explicit-cross-domain-links.js
Browse files Browse the repository at this point in the history
It has transpired that the explicit-cross-domain-links.js script was not
set up correctly which is breaking our cross domain tracking currently.

The source of the issue is the ga parameter which contains a timestamp
which actually expires and becomes invalid after two minutes.

The way the script worked previously was that the relevant cross-domain
links and forms on the page were decorated with the _ga and
cookie_consent parameters on page load. Doing it on page load is an
issue due to the time-sensitive nature of the _ga parameter, as
mentioned above.

This changes the way the script works so that instead of decorating
crossdomain links and forms with additional parameters on page load, we
decorate them when they are interacted with.
Besides ensuring that the _ga param is always valid and up-to-date, this
also makes it so that there is no longer a need to listen for cookie
banner events, so I also removed the cookie-reject event as part of this
commit.
  • Loading branch information
danacotoran committed Dec 9, 2021
1 parent c8ae3ea commit 3684c2d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 131 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
useful summary for people upgrading their application, not a replication
of the commit log.

## Unreleased
## Unreleased

* Alter use of pseudo-underline mixin to allow for different button sizes ([#2501](https://github.com/alphagov/govuk_publishing_components/pull/2501))
* Re-work explicit-cross-domain-links.js ([PR #2502](https://github.com/alphagov/govuk_publishing_components/pull/2502))

## 27.16.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,79 @@

GOVUK.Modules.ExplicitCrossDomainLinks = function () {
this.start = function ($module) {
var element = $module[0]
this.element = $module[0]
this.attribute = 'href'
this.attributeValue = this.element.getAttribute(this.attribute)
this.eventType = 'click'
if (!this.attributeValue) {
this.attribute = 'action'
this.attributeValue = this.element.getAttribute(this.attribute)
this.eventType = 'submit'
}

var cookieBannerEngaged = GOVUK.cookie('cookies_preferences_set')
this.handleEvent = this.handleEvent.bind(this)
this.handleCookiesAccepted = this.handleCookiesAccepted.bind(this)
// Listens for the 'submit' event if the element is a form, and the 'click' event if it is a link
this.element.addEventListener(this.eventType, this.handleEvent)
}

// If not engaged, append only ?cookie_consent=not-engaged
// If engaged and rejected, append only ?cookie_consent=reject
// If engaged and accepted usage, append ?_ga=clientid if available and cookie_consent=accept
this.handleEvent = function (e) {
// prevent default: we want the link href and/or form action to be decorated before we navigate away
e.preventDefault()
var cookieBannerEngaged = GOVUK.cookie('cookies_preferences_set')
var cookieConsent = GOVUK.getConsentCookie()

if (cookieBannerEngaged !== 'true') {
this.decorate(element, 'cookie_consent=not-engaged')
this.start = this.start.bind(this, $module)

// if the user has not engaged with the cookie banner yet, listen for the cookie consent accept/reject events
// re-start the module if cookies are accepted or rejected on the current page – setting cookie preferences does not reload the page
window.addEventListener('cookie-consent', this.start)
window.addEventListener('cookie-reject', this.start)
return
// If not engaged, append only ?cookie_consent=not-engaged
this.decorate(this.element, 'cookie_consent=not-engaged', this.attribute)
} else if (cookieConsent && cookieConsent.usage === true) {
this.handleCookiesAccepted()
} else {
this.decorate(this.element, 'cookie_consent=reject', this.attribute)
}
var cookieConsent = GOVUK.getConsentCookie()
if (cookieConsent && cookieConsent.usage === false) {
this.decorate(element, 'cookie_consent=reject')
return

// remove the event listener to avoid an infinite loop
this.element.removeEventListener(this.eventType, this.handleEvent)

// if the element is a form, submit it. If it is a link, click it
if (this.eventType === 'submit') {
this.element.submit()
} else {
this.element.click()
}
}

this.decorate(element, 'cookie_consent=accept')
this.handleCookiesAccepted = function () {
// If the cookie banner was engaged and usage cookie accepted, append ?_ga=clientid if available and cookie_consent=accept
var element = this.element
var attribute = this.attribute
this.decorate(element, 'cookie_consent=accept', attribute)

if (!global.ga) { return }
if (!global.ga) {
return
}

global.ga(function () {
var trackers = global.ga.getAll()

if (!trackers.length) { return }

var linker = new global.gaplugins.Linker(trackers[0])
var attrValue = element.getAttribute(attribute)

var attrAction = element.getAttribute('action')
if (attrAction) {
element.setAttribute('action', linker.decorate(attrAction))
}

var attrHref = element.getAttribute('href')
if (attrHref) {
element.href = linker.decorate(attrHref)
}
element.setAttribute(attribute, linker.decorate(attrValue))
})
}

this.decorate = function (element, param) {
var attribute = 'href'
this.decorate = function (element, param, attribute) {
var attributeValue = element.getAttribute(attribute)
var cookieConsentParameterPattern = /cookie_consent=[^&]*/
var paramIsCookieConsent = param.match(cookieConsentParameterPattern)

if (!attributeValue) {
attribute = 'action'
attributeValue = element.getAttribute(attribute)
}

if (!attributeValue) { return }

var attributeHasCookieConsent = attributeValue.match(cookieConsentParameterPattern)

if (attributeHasCookieConsent && paramIsCookieConsent) {
// if the decorate function has received a cookie_consent parameter, but the target element already has a cookie_consent parameter, replace the existing parameter with the new value
attributeValue = attributeValue.replace(cookieConsentParameterPattern, param)
if (attributeValue.includes('?')) {
attributeValue += '&' + param
} else {
// otherwise, simply append the parameter to the target element href query string
if (attributeValue.includes('?')) {
attributeValue += '&' + param
} else {
attributeValue += '?' + param
}
attributeValue += '?' + param
}

element.setAttribute(attribute, attributeValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
this.$module.cookieBannerConfirmationMessage.focus()
window.GOVUK.cookie('cookies_preferences_set', 'true', { days: 365 })
window.GOVUK.setDefaultConsentCookie()
window.GOVUK.triggerEvent(window, 'cookie-reject')
}

CookieBanner.prototype.showConfirmationMessage = function () {
Expand Down
2 changes: 0 additions & 2 deletions docs/javascript-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ This functionality runs like this:
- if cookies have been consented, the module calls the rest of its code and carries on as normal
- if cookies have not been consented, the listener is created and calls the rest of the module when the `cookie-consent` event is fired by the cookie banner

If a module has functionality which is dependent on rejecting cookies, that module should listen for the `cookie-reject` event. This event is fired by the cookie banner when the user rejects cookies.

### Module structure

A module must add its constructor to `GOVUK.Modules` and it must have an `init` method. The simplest module looks like:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,37 @@ describe('Explicit cross-domain linker', function () {
delete window.gaplugins
})

describe('links', function () {
describe('when a cross-domain link is clicked', function () {
beforeEach(function () {
element = $('<a href="/somewhere">')
element = $('<a href="#">')
})

it('modifies the link href to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is "false"', function () {
GOVUK.cookie('cookies_preferences_set', 'false')
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=not-engaged')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=not-engaged')
expect(window.location.href).toContain('?cookie_consent=not-engaged')
})

it('modifies the link href to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is not set', function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=not-engaged')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=not-engaged')
expect(window.location.href).toContain('?cookie_consent=not-engaged')
})

it('modifies the link href to append cookie_consent parameter "reject" if usage cookies have been rejected', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: false })
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=reject')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=reject')
expect(window.location.href).toContain('?cookie_consent=reject')
})

describe('user has accepted cookies', function () {
Expand All @@ -73,51 +82,39 @@ describe('Explicit cross-domain linker', function () {
trackers = [{ ga_mock: 'foobar' }]

explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept&_ga=abc123')
expect(element.attr('href')).toEqual('#?cookie_consent=accept&_ga=abc123')
expect(window.location.href).toContain('?cookie_consent=accept&_ga=abc123')
})

it('modifies the link href to only append cookie_consent "accept" if there are no trackers', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: true })
trackers = []

explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=accept')
expect(window.location.href).toContain('?cookie_consent=accept')
})

it('modifies the link href to only append cookie_consent "accept" if ga is not initalised on window', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
explicitCrossDomainLinks.start(element)
expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
})
})

describe('user has interacted with the cookie banner on the current page', function () {
beforeEach(function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
GOVUK.cookie('cookies_preferences_set', 'true')
})
it('modifies the link href to append cookie_consent parameter "accept" if the cookie-consent event was fired', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
window.GOVUK.triggerEvent(window, 'cookie-consent')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=accept')
})

it('modifies the link href to append cookie_consent parameter "reject" if the cookie-reject event was fired', function () {
GOVUK.setConsentCookie({ usage: false })
window.GOVUK.triggerEvent(window, 'cookie-reject')

expect(element.attr('href')).toEqual('/somewhere?cookie_consent=reject')
expect(element.attr('href')).toEqual('#')
window.GOVUK.triggerEvent(element[0], 'click')
expect(element.attr('href')).toEqual('#?cookie_consent=accept')
expect(window.location.href).toContain('?cookie_consent=accept')
})
})
})

describe('forms', function () {
describe('when a cross-domain form is submitted', function () {
beforeEach(function () {
element = $('<form method="POST" action="/somewhere">' +
'<input type="hidden" name="key" value="value" />' +
Expand All @@ -128,19 +125,26 @@ describe('Explicit cross-domain linker', function () {
it('modifies the form action to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is "false"', function () {
GOVUK.cookie('cookies_preferences_set', 'false')
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=not-engaged')
})

it('modifies the form action to append cookie_consent parameter "not-engaged" if cookies_preferences_set cookie is not set', function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=not-engaged')
})

it('modifies the form action to append cookie_consent parameter "reject" if usage cookies have been rejected', function () {
GOVUK.cookie('cookies_preferences_set', 'true')
GOVUK.setConsentCookie({ usage: false })
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=reject')
})

Expand All @@ -150,6 +154,8 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
trackers = [{ ga_mock: 'foobar' }]
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept&_ga=abc123')
})

Expand All @@ -158,6 +164,8 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
trackers = []
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})

Expand All @@ -166,30 +174,10 @@ describe('Explicit cross-domain linker', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
explicitCrossDomainLinks.start(element)
expect(element.attr('action')).toEqual('/somewhere')
window.GOVUK.triggerEvent(element[0], 'submit')
expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})
})

describe('user has interacted with the cookie banner on the current page', function () {
beforeEach(function () {
GOVUK.cookie('cookies_preferences_set', null)
explicitCrossDomainLinks.start(element)
GOVUK.cookie('cookies_preferences_set', 'true')
})
it('modifies the form action to append cookie_consent parameter "accept" if the cookie-consent event was fired', function () {
GOVUK.setConsentCookie({ usage: true })
window.ga = undefined
window.GOVUK.triggerEvent(window, 'cookie-consent')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=accept')
})

it('modifies the form action to append cookie_consent parameter "reject" if the cookie-reject event was fired', function () {
GOVUK.setConsentCookie({ usage: false })
window.GOVUK.triggerEvent(window, 'cookie-reject')

expect(element.attr('action')).toEqual('/somewhere?cookie_consent=reject')
})
})
})
})
26 changes: 0 additions & 26 deletions spec/javascripts/govuk_publishing_components/modules.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,6 @@ describe('GOVUK Modules', function () {
}
GOVUK.Modules.TestCookieDependencyModule = TestCookieDependencyModule

// GOV.UK Frontend Module that depends on rejected cookies to start
function TestCookieRejectDependencyModule (element) {
this.element = element
}
TestCookieRejectDependencyModule.prototype.init = function () {
this.startModule = this.startModule.bind(this)
window.addEventListener('cookie-reject', this.startModule)
}
TestCookieRejectDependencyModule.prototype.startModule = function () {
window.removeEventListener('cookie-reject', this.startModule)
callbackFrontendModule(this.element)
}
GOVUK.Modules.TestCookieRejectDependencyModule = TestCookieRejectDependencyModule

container = $('<div></div>')
})

Expand All @@ -165,7 +151,6 @@ describe('GOVUK Modules', function () {
delete GOVUK.Modules.GovukTestAlertPublishingAndFrontendModule
delete GOVUK.Modules.TestAlertPublishingAndFrontendModule
delete GOVUK.Modules.TestCookieDependencyModule
delete GOVUK.Modules.TestCookieRejectDependencyModule

container.remove()
})
Expand Down Expand Up @@ -243,17 +228,6 @@ describe('GOVUK Modules', function () {
expect(callbackFrontendModule.calls.count()).toBe(1)
})

it('starts delayed modules once cookies have been rejected', function () {
var module = $('<div data-module="test-cookie-reject-dependency-module"></div>')
container.append(module)
$('body').append(container)

GOVUK.modules.start(container)
expect(callbackFrontendModule.calls.count()).toBe(0)
window.GOVUK.triggerEvent(window, 'cookie-reject')
expect(callbackFrontendModule.calls.count()).toBe(1)
})

it('starts multiple delayed modules once cookies have been consented', function () {
var module1 = $('<div data-module="test-cookie-dependency-module"></div>')
var module2 = $('<div data-module="test-cookie-dependency-module"></div>')
Expand Down

0 comments on commit 3684c2d

Please sign in to comment.