Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT Add analytics and cookie banner #49

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _includes/layouts/homepage.njk
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "layouts/product.njk" %}
{% extends "layouts/main.njk" %}

{% block main %}
<main id="main-content" role="main" {%- if mainLang %} lang="{{ mainLang }}"{% endif %}>
Expand Down
27 changes: 27 additions & 0 deletions _includes/layouts/main.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{# Extend GOV.UK Eleventy Plugin base layout #}
{% extends "layouts/base.njk" %}

{% from "macros/cookie-banner.njk" import cookieBanner %}

{% block bodyStart %}
<a id="top"></a>
<script>
/**
* If cookie policy changes and/or the user preferences object format needs to
* change, bump this version up afterwards. The user should then be shown the
* banner again to consent to the new policy.
*
* Note that because isValidCookieConsent checks that the version in the user's
* cookie is equal to or greater than this number, you should be careful to
* check backwards compatibility when changing the object format.
*/
window.GDS_CONSENT_COOKIE_VERSION = 2;
</script>

{{ cookieBanner() }}

{% endblock %}

{% block scripts %}
<script src="/assets/javascripts/application.bundle.js" type="module"></script>
{% endblock %}
147 changes: 147 additions & 0 deletions _includes/macros/cookie-banner.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
{% macro cookieBanner(params) %}

{% from "govuk/components/cookie-banner/macro.njk" import govukCookieBanner %}

{%- set category -%}
{%- if params.category -%}
{{ params.category }}
{%- else -%}
analytics
{%- endif -%}
{%- endset -%}

{%- set title -%}
{%- if params.title -%}
{{ params.title }}
{%- else -%}
Cookies on GOV.UK Design System
{%- endif -%}
{%- endset -%}

{%- set html %}
{% if params.html %}
{{ params.html | safe }}
{% else %}
<p class="govuk-body">We’d like to use analytics cookies so we can understand how you use the Design System and make improvements.</p>
<p class="govuk-body">We also use essential cookies to remember if you’ve accepted analytics cookies.</p>
{% endif %}
{% endset %}

{%- set acceptHtml %}
{% if params.acceptHtml %}
{{ params.acceptHtml | safe }}
{% else %}
<p class="govuk-body">You’ve accepted analytics cookies. You can <a class="govuk-link" href="/cookies/">change your cookie settings</a> at any time.</p>
{% endif %}
{% endset %}

{%- set rejectHtml %}
{% if params.rejectHtml %}
{{ params.rejectHtml | safe }}
{% else %}
<p class="govuk-body">You’ve rejected analytics cookies. You can <a class="govuk-link" href="/cookies/">change your cookie settings</a> at any time.</p>
{% endif %}
{% endset %}

{{ govukCookieBanner({
ariaLabel: title,
hidden: true,
classes: params.classes,
attributes: {
"data-module": "govuk-cookie-banner",
"data-cookie-category": category
},
messages: [
{
headingText: title,
html: html,
actions: [
{
text: "Accept " + category + " cookies",
type: "button",
classes: "js-cookie-banner-accept"
},
{
text: "Reject " + category + " cookies",
type: "button",
classes: "js-cookie-banner-reject"
},
{
text: "View cookies",
href: "/cookies/"
}
],
classes: "app-width-container js-cookie-banner-message"
},
{
html: acceptHtml,
role: "alert",
hidden: true,
actions: [
{
text: "Hide cookie message",
type: "button",
classes: "js-cookie-banner-hide js-cookie-banner-hide--accept"
}
],
classes: "js-cookie-banner-confirmation-accept app-width-container"
},
{
html: rejectHtml,
role: "alert",
hidden: true,
actions: [
{
text: "Hide cookie message",
type: "button",
classes: "js-cookie-banner-hide js-cookie-banner-hide--reject"
}
],
classes: "js-cookie-banner-confirmation-reject app-width-container"
}
]
}) }}

{# Inline script to show the cookie banner as soon as possible,
to avoid a high cumulative layout shift (CLS) score https://web.dev/cls/ #}
<script>
(function () {
// Skip early setup when cookie banner component is not supported
if (!('noModule' in HTMLScriptElement.prototype)) {
return
}

/**
* Check the cookie preferences object.
*
* If the consent object is not present, malformed, or incorrect version,
* returns false, otherwise returns true.
*
* This is also duplicated in cookie-functions.js - the two need to be kept in sync
*/
function isValidConsentCookie (options) {
return (options && options.version >= window.GDS_CONSENT_COOKIE_VERSION)
}

function categoryIsNull (options) {
return (options && options.{{ category }} === null)
}

// Don't show the banner on the cookies page
if (window.location.pathname !== "/cookies/") {
// Show the banner if there is no consent cookie or if it is outdated
var currentConsentCookie = document.cookie.match(new RegExp('(^| )design_system_cookies_policy=([^;]+)'))

const cookieData = currentConsentCookie && JSON.parse(currentConsentCookie[2]);
const cookieNotSet = (!currentConsentCookie || !isValidConsentCookie(cookieData))
const categoryNotSet = isValidConsentCookie(cookieData) && categoryIsNull(cookieData)

if (cookieNotSet || categoryNotSet) {
var cookieBanner = document.querySelector("[data-cookie-category='{{ category }}']")
cookieBanner.removeAttribute('hidden')
}
}
})()
</script>

{% endmacro %}
57 changes: 57 additions & 0 deletions docs/cookies.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{% extends "layouts/base.njk" %}

{% block content %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-three-quarters-from-desktop">
{% include 'partials/header.njk' %}

<section class="doc-what-are-cookies">
<h2 class="govuk-heading-l">What are cookies</h2>
<p class="govuk-body">Cookies are small files saved on your phone, tablet or computer when you visit a website.</p>
<p class="govuk-body">We use cookies to make this site work and collect information about how us use our service.</p>
</section>

{% if options.googleTagManagerIdentifier %}
<section class="doc-cookies-page-controls">
<h2 class="govuk-heading-l">Change your cookie settings</h2>
<div id="refresh-page-message">
<p class="govuk-body">We cannot change your cookie settings at the moment because JavaScript is not running in your browser. To fix this, try:</p>
<ul class="govuk-list govuk-list--bullet">
<li>turning on JavaScript in your browser settings</li>
<li>reloading this page</li>
</ul>
</div>
<form class="govuk-visually-hidden" action="/form-handler" method="post" novalidate>
{{ govukRadios({
classes: "govuk-radios--inline",
name: "cookies[analytics]",
idPrefix: "cookies-analytics",
fieldset: {
legend: {
text: "Do you want to accept analytics cookies?",
classes: "govuk-fieldset__legend--s"
}
},
items: [
{
value: "yes",
text: "Yes"
},
{
value: "no",
text: "No"
}
]
}) }}

{{ govukButton({
text: "Save cookie settings"
}) }}
</form>
</section>
{% else %}
<p class="govuk-body">Currently there are no cookies used on this service.</p>
{% endif %}
</div>
</div>
{% endblock %}
29 changes: 29 additions & 0 deletions docs/javascripts/application.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable no-new */

import { createAll } from 'govuk-frontend'

import { loadAnalytics } from './components/analytics.mjs'
import CookieBanner from './components/cookie-banner.mjs'
import {
getConsentCookie,
isValidConsentCookie,
removeUACookies
} from './components/cookie-functions.mjs'
import CookiesPage from './components/cookies-page.mjs'

// Cookies and analytics
createAll(CookieBanner)
createAll(CookiesPage)

// Check for consent before initialising analytics
const userConsent = getConsentCookie()
if (userConsent && isValidConsentCookie(userConsent) && userConsent.analytics) {
loadAnalytics()

// Remove UA cookies if the user previously had them set or Google attempts
// to set them
removeUACookies()
}

// Initialise cookie page
createAll(CookiesPage)
79 changes: 79 additions & 0 deletions docs/javascripts/components/analytics.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// @ts-nocheck

// eslint-disable-next-line jsdoc/require-jsdoc
export function loadAnalytics() {
if (!window.ga || !window.ga.loaded) {
// Load gtm script
// Script based on snippet at https://developers.google.com/tag-manager/quickstart
// prettier-ignore
;(function (w, d, s, l, i) {
w[l] = w[l] || []
w[l].push({
'gtm.start': new Date().getTime(),
event: 'gtm.js'
})

const j = d.createElement(s)
const dl = l !== 'dataLayer' ? `&l=${l}` : ''

j.async = true
j.src = `https://www.googletagmanager.com/gtm.js?id=${i}${dl}`
document.head.appendChild(j)
})(window, document, 'script', 'dataLayer', 'GTM-53XG2JT')
}
}

/**
* Push to Google Analytics
*
* @param {object} payload - Google Analytics payload
*/
export function addToDataLayer(payload) {
// @ts-expect-error Property does not exist on window
window.dataLayer = window.dataLayer || []
// @ts-expect-error Property does not exist on window
window.dataLayer.push(payload)
}

/**
* Strip possible personally identifiable information (PII)
*
* @param {string} string - Input string
* @returns {string} Output string
*/
export function stripPossiblePII(string) {
// Try to detect emails, postcodes, and NI numbers, and redact them.
// Regexes copied from GTM variable 'JS - Remove PII from Hit Payload'
string = string.replace(/[^\s=/?&]+(?:@|%40)[^\s=/?&]+/g, '[REDACTED EMAIL]')
string = string.replace(
/\b[A-PR-UWYZ][A-HJ-Z]?[0-9][0-9A-HJKMNPR-Y]?(?:[\s+]|%20)*[0-9](?!refund)[ABD-HJLNPQ-Z]{2,3}\b/gi,
'[REDACTED POSTCODE]'
)
string = string.replace(
/^\s*[a-zA-Z]{2}(?:\s*\d\s*){6}[a-zA-Z]?\s*$/g,
'[REDACTED NI NUMBER]'
)
// If someone has typed in a number it's likely not related so redact it
string = string.replace(/[0-9]+/g, '[REDACTED NUMBER]')
return string
}

/**
* Translate list of search results to
* format compatiable with GA4 ecommerce
* `items` attribute
*
* @param {Array} searchResults - Array of search results
* @param {string} searchTerm - Search string entered by user
* @returns {Array} items - Array of `items`
*/
export function translateToItems(searchResults, searchTerm) {
const items = searchResults.map((result, key) => ({
name: result.title,
category: result.section,
list: searchTerm, // Used to match an searchTerm with results
position: key + 1
}))

return items
}
Loading