From ac0e9ca303206b58f7ea9bead162869fbef32607 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Mon, 25 Mar 2024 16:34:46 +0000 Subject: [PATCH] Update TNA Frontend --- package-lock.json | 8 +- package.json | 2 +- src/scripts/main.js | 14 +- src/scripts/tmp/analytics.mjs | 398 ++++++++++++---------------------- 4 files changed, 152 insertions(+), 270 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39a7baf7..d5346429 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@nationalarchives/frontend": "0.1.47" + "@nationalarchives/frontend": "0.1.48" }, "devDependencies": { "@babel/core": "^7.22.11", @@ -1955,9 +1955,9 @@ } }, "node_modules/@nationalarchives/frontend": { - "version": "0.1.47", - "resolved": "https://registry.npmjs.org/@nationalarchives/frontend/-/frontend-0.1.47.tgz", - "integrity": "sha512-9bt7aogHLTiSwL/0Y+XnE9z/BLYRUCGwB1lw/hGVbaEVd8lHbn1iqlbRQW6DpIQp8WUxhfMEnQBHRfEoEyt5tw==" + "version": "0.1.48", + "resolved": "https://registry.npmjs.org/@nationalarchives/frontend/-/frontend-0.1.48.tgz", + "integrity": "sha512-iAEKijoPBhpnhqDx/+bYVA3ZYyuYzHLSyHIBYiRkTAClSag1nMdR69UzvUJ3Mqx8jIIEhuERfa+16k9x1+JZZw==" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", diff --git a/package.json b/package.json index 2fb44680..d7093c2c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "prettier --write src && stylelint --fix 'src/styles/**/*.scss' && eslint --fix 'src/scripts/**/*.js'" }, "dependencies": { - "@nationalarchives/frontend": "0.1.47" + "@nationalarchives/frontend": "0.1.48" }, "devDependencies": { "@babel/core": "^7.22.11", diff --git a/src/scripts/main.js b/src/scripts/main.js index ba2db150..5e8453ab 100644 --- a/src/scripts/main.js +++ b/src/scripts/main.js @@ -3,22 +3,16 @@ import { GA4, helpers, } from "@nationalarchives/frontend/nationalarchives/analytics.mjs"; -// import { -// GA4, -// helpers, -// } from "./tmp/analytics.mjs"; +// import { GA4, helpers } from "./tmp/analytics.mjs"; import "./modules/theme-switcher"; initAll(); -// const GA4 = window.TNAFrontendAnalytics.GA4; -// const helpers = window.TNAFrontendAnalytics.helpers; - const ga4Id = document.documentElement.getAttribute("data-ga4id"); if (ga4Id) { const analytics = new GA4({ id: ga4Id }); - analytics.addListener(".etna-article__sidebar", "sidebar", [ + analytics.addListeners(".etna-article__sidebar", "sidebar", [ { eventName: "section.jump_to", targetElement: ".etna-article__sidebar-item", @@ -29,7 +23,7 @@ if (ga4Id) { }, ]); - analytics.addListener(".etna-article", "article", [ + analytics.addListeners(".etna-article", "article", [ { eventName: "section.toggle", targetElement: ".etna-article__section-button", @@ -41,7 +35,7 @@ if (ga4Id) { }, ]); - analytics.addListener(document.documentElement, "document", [ + analytics.addListeners(document.documentElement, "document", [ { eventName: "double_click", on: "dblclick", diff --git a/src/scripts/tmp/analytics.mjs b/src/scripts/tmp/analytics.mjs index 19f6cacf..d03f7785 100644 --- a/src/scripts/tmp/analytics.mjs +++ b/src/scripts/tmp/analytics.mjs @@ -1,219 +1,33 @@ import Cookies from "@nationalarchives/frontend/nationalarchives/lib/cookies.mjs"; - -const getXPathTo = ($element) => { - if ($element.id !== "") { - return 'id("' + $element.id + '")'; - } - if ($element === document.body) { - return $element.tagName; - } - let ix = 0; - const siblings = $element.parentNode.childNodes; - for (let i = 0; i < siblings.length; i++) { - const sibling = siblings[i]; - if (sibling === $element) - return ( - getXPathTo($element.parentNode) + - "/" + - $element.tagName + - "[" + - (ix + 1) + - "]" - ); - if (sibling.nodeType === 1 && sibling.tagName === $element.tagName) ix++; - } -}; - -const includesAny = (arr, values) => values.some((v) => arr.includes(v)); - -const getClosestHeading = ($element) => { - let heading = ""; - let $search = $element; - do { - while ($search.previousElementSibling) { - $search = $search.previousElementSibling; - if ( - ["h1", "h2", "h3", "h4", "h5", "h6"].includes($search.tagName) || - ($search.classList.length && - includesAny(Array.from($search.classList), [ - "tna-heading-xl", - "tna-heading-l", - "tna-heading-m", - "tna-heading-s", - ])) - ) { - heading = $search.innerText; - break; - } - } - $search = $search.parentElement; - } while ($search.parentElement && !heading); - return heading; -}; - -const valueGetters = { - text: ($el) => $el.innerText, - html: ($el) => $el.innerHTML, - value: ($el) => $el.value, - index: ($el, $scope, event, index) => index, - checked: ($el) => ($el.checked ? "checked" : "unchecked"), - expanded: ($el) => { - const expanded = $el.getAttribute("aria-expanded"); - if (expanded === null) { - return null; - } - return expanded.toString() === "true" ? "opened" : "closed"; - }, - closestHeading: ($el) => getClosestHeading($el), -}; +import { + getXPathTo, + getClosestHeading, + valueGetters, +} from "@nationalarchives/frontend/nationalarchives/lib/analytics-helpers.mjs"; +import BreadcrumbAnalytics from "@nationalarchives/frontend/nationalarchives/components/breadcrumbs/analytics.js"; +import CheckboxesAnalytics from "@nationalarchives/frontend/nationalarchives/components/checkboxes/analytics.js"; +import FooterAnalytics from "@nationalarchives/frontend/nationalarchives/components/footer/analytics.js"; +import GlobalHeaderAnalytics from "@nationalarchives/frontend/nationalarchives/components/global-header/analytics.js"; +import HeaderAnalytics from "@nationalarchives/frontend/nationalarchives/components/header/analytics.js"; +import HeroAnalytics from "@nationalarchives/frontend/nationalarchives/components/hero/analytics.js"; +import PictureAnalytics from "@nationalarchives/frontend/nationalarchives/components/picture/analytics.js"; +import RadiosAnalytics from "@nationalarchives/frontend/nationalarchives/components/radios/analytics.js"; +import SearchFieldAnalytics from "@nationalarchives/frontend/nationalarchives/components/search-field/analytics.js"; +import TextInputAnalytics from "@nationalarchives/frontend/nationalarchives/components/text-input/analytics.js"; +import TextareaAnalytics from "@nationalarchives/frontend/nationalarchives/components/textarea/analytics.js"; const componentAnalytics = [ - { - scope: ".tna-global-header", - areaName: "header", - events: [ - { - eventName: "toggle", - targetElement: ".tna-global-header__navigation-button", - on: "click", - data: { - state: valueGetters.expanded, - }, - }, - { - eventName: "logo.click", - targetElement: ".tna-global-header__logo", - on: "click", - rootData: { - data_component_name: "Header", - data_link_type: "Logo", - data_link: "The National Archives", - }, - }, - { - eventName: "primary_link.click", - targetElement: ".tna-global-header__navigation-item-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Header", - data_link_type: "Menu", - data_section: valueGetters.text, - data_position: valueGetters.index, - data_link: valueGetters.text, - }, - }, - { - eventName: "secondary_link.click", - targetElement: ".tna-global-header__top-navigation-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Header", - data_link_type: "Icon", - data_position: valueGetters.index, - data_link: valueGetters.text, - }, - }, - ], - }, - { - scope: ".tna-footer", - areaName: "footer", - events: [ - { - eventName: "link.click", - targetElement: ".tna-footer__navigation-block-item-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Link", - data_section: valueGetters.closestHeading, - data_position: valueGetters.index, - data_link: valueGetters.text, - }, - }, - { - eventName: "social_link.click", - targetElement: ".tna-footer__social-item-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Icon", - data_section: "Social media", - data_position: valueGetters.index, - data_link: ($el) => $el.getAttribute("data-name"), - }, - }, - { - eventName: "legal_link.click", - targetElement: ".tna-footer__legal-item-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Link", - data_section: "Legal information", - data_position: valueGetters.index, - data_link: valueGetters.text, - }, - }, - { - eventName: "mailing_list.click", - targetElement: ".tna-footer__mailing-list a.tna-button", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Button", - data_section: "Mailing list", - data_link: valueGetters.text, - }, - }, - { - eventName: "ogl.click", - targetElement: ".tna-footer__licence p a.tna-footer__link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Link", - data_section: "OGL", - data_link: valueGetters.text, - }, - }, - { - eventName: "govuk.click", - targetElement: ".tna-footer__govuk-link", - on: "click", - data: { - value: valueGetters.text, - }, - rootData: { - data_component_name: "Footer", - data_link_type: "Logo", - data_section: "GOV.UK", - data_link: valueGetters.text, - }, - }, - ], - }, + ...BreadcrumbAnalytics, + ...CheckboxesAnalytics, + ...FooterAnalytics, + ...GlobalHeaderAnalytics, + ...HeaderAnalytics, + ...HeroAnalytics, + ...PictureAnalytics, + ...RadiosAnalytics, + ...SearchFieldAnalytics, + ...TextInputAnalytics, + ...TextareaAnalytics, ]; class EventTracker { @@ -224,26 +38,56 @@ class EventTracker { events = []; /** @protected */ - start = new Date(); + startTime = new Date(); /** @protected */ prefix = "tna"; - constructor(prefix) { + /** @protected */ + addTrackingCode = true; + + constructor(options = {}) { + const { prefix = null, addTrackingCode = true } = options; if (prefix) { this.prefix = prefix; } + this.addTrackingCode = addTrackingCode; } + start(initAll) { + if (!navigator.doNotTrack || navigator.doNotTrack !== 1) { + if (this.cookies.isPolicyAccepted("usage")) { + this.enableTracking(); + } + this.cookies.on("changePolicy", (policies) => { + if (Object.hasOwn(policies, "usage")) { + if (policies["usage"]) { + this.enableTracking(); + } else { + this.disableTracking(); + } + } + }); + if (initAll) { + this.initAll(); + } + } + } + + enableTracking() {} + + disableTracking() {} + /** * Initialise all TNA Frontend component analytics. */ initAll() { componentAnalytics.forEach((componentConfig) => { - this.addListener( + this.addListeners( componentConfig.scope, componentConfig.areaName, componentConfig.events, + componentConfig.rootEventName || "", ); }); } @@ -252,9 +96,10 @@ class EventTracker { * Add an event listener. * @param {String|HTMLElement} scope - The element to which the listener is scoped. * @param {String} areaName - The name of the component to pass on to the tracker. - * @param {{eventName: String, targetElement: String|undefined, on: String, data: {value: Function|String|undefined, state: Function|String|undefined, [String]: any}}[]} events - The configuration of events to track along with their optional value and state which can be computed. + * @param {{eventName: String, targetElement: String|undefined, on: String, data: {value: Function|String|undefined, state: Function|String|undefined, group: Function|String|undefined, [String]: String|Integer}, rootData:{[String]: Function|String}}[]} events - The configuration of events to track along with their optional value and state which can be computed. + * @param {String} rootEventName - The event name to use if specified (prefix). */ - addListener(scope, areaName, events) { + addListeners(scope, areaName, events, rootEventName = "") { let scopeArray; if (typeof scope === "string") { scopeArray = Array.from(document.querySelectorAll(scope)); @@ -276,7 +121,7 @@ class EventTracker { this.attachListener( $el, $scope, - this.generateEventName(areaName, eventConfig), + rootEventName, eventConfig, scope, areaName, @@ -287,7 +132,7 @@ class EventTracker { this.attachListener( $scope, $scope, - this.generateEventName(areaName, eventConfig), + rootEventName, eventConfig, scope, areaName, @@ -304,21 +149,36 @@ class EventTracker { } /** @protected */ - attachListener($el, $scope, eventName, eventConfig, scope, areaName, index) { + attachListener( + $el, + $scope, + rootEventName, + eventConfig, + scope, + areaName, + index, + ) { const { on, data, targetElement, rootData = {} } = eventConfig; $el.addEventListener(on, (event) => this.recordEvent( - eventName, + rootEventName + ? `${this.prefix}.${rootEventName}` + : this.generateEventName(areaName, eventConfig), { ...data, - value: this.computedValue(data.value, $el, $scope, event, index), - state: this.computedValue(data.state, $el, $scope, event, index), - group: this.computedValue(data.group, $el, $scope, event, index), - xPath: getXPathTo($scope), + name: this.generateEventName(areaName, eventConfig), + value: data?.value + ? this.computedValue(data.value, $el, $scope, event, index) + : null, + state: data?.state + ? this.computedValue(data.state, $el, $scope, event, index) + : null, + group: data?.group + ? this.computedValue(data.group, $el, $scope, event, index) + : null, + xPath: getXPathTo($el), targetElement: targetElement, - // timestamp: new Date().toISOString(), - // uri: window.location.pathname, - timeSincePageLoad: new Date() - this.start, + timeSincePageLoad: new Date() - this.startTime, index, scope, areaName, @@ -337,7 +197,7 @@ class EventTracker { computedValue(value, $el, $scope, event, index) { return typeof value === "function" ? value.call(this, $el, $scope, event, index) - : value || null; + : value ?? null; } /** @protected */ @@ -372,37 +232,52 @@ class EventTracker { * @public */ class GA4 extends EventTracker { + /** @protected */ trackingCodeAdded = false; + + /** @protected */ trackingEnabled = false; + + /** @protected */ gTagId; - constructor(id = null, options = {}) { + constructor(options = {}) { if (GA4._instance) { return GA4._instance; } + const { + id = null, + prefix = null, + initAll = true, + addTrackingCode = true, + } = options; if (!id) { throw Error("ID was not specified"); } - const { prefix = null, initAll = true } = options; - super(prefix); + super({ prefix, addTrackingCode }); GA4._instance = this; this.gTagId = id; + this.ga4Disable = `ga-disable-${this.gTagId}`; window.dataLayer = window.dataLayer || []; - if (this.cookies.isPolicyAccepted("usage")) { - this.enableTracking(); - } - this.cookies.on("changePolicy", (policies) => { - if (Object.hasOwn(policies, "usage")) { - if (policies["usage"]) { - this.enableTracking(); - } else { - this.disableTracking(); - } - } - }); - if (initAll) { - this.initAll(); + if (!this.cookies.isPolicyAccepted("usage")) { + window[this.ga4Disable] = true; + this.cookies.set(this.ga4Disable, "true"); } + + /** + * _______ ______ _____ _______ + * |__ __| | ____| / ____| |__ __| + * | | | |__ | (___ | | + * | | | __| \___ \ | | + * | | | |____ ____) | | | + * |_| |______| |_____/ |_| + */ + + this.start(initAll); + } + + destroy() { + GA4._instance = null; } /** @protected */ @@ -433,9 +308,9 @@ class GA4 extends EventTracker { /** @protected */ enableTracking() { if (!this.trackingEnabled) { - window["ga-disable-GA_MEASUREMENT_ID"] = false; - this.trackingEnabled = true; - if (!this.trackingCodeAdded) { + window[this.ga4Disable] = false; + this.cookies.set(this.ga4Disable, "false"); + if (!this.trackingCodeAdded && this.addTrackingCode) { this.pushToDataLayer({ "gtm.start": new Date().getTime(), event: "gtm.js", @@ -444,20 +319,33 @@ class GA4 extends EventTracker { const script = document.createElement("script"); script.async = true; script.src = `https://www.googletagmanager.com/gtm.js?id=${this.gTagId}&l=dataLayer`; - firstScript.parentNode.insertBefore(script, firstScript); + if (firstScript) { + firstScript.parentNode.insertBefore(script, firstScript); + } else { + document.head.appendChild(script); + } this.trackingCodeAdded = true; - this.pushToDataLayer(this.getTnaMetaTags()); + const tnaMetaTags = this.getTnaMetaTags(); + if (Object.keys(tnaMetaTags).length) { + this.pushToDataLayer(tnaMetaTags); + } } - this.gtag("set", { allow_google_signals: true }); + this.trackingEnabled = true; } } /** @protected */ disableTracking() { if (this.trackingEnabled) { - window["ga-disable-GA_MEASUREMENT_ID"] = true; - this.gtag("set", { allow_google_signals: false }); + window[this.ga4Disable] = true; + this.cookies.set(this.ga4Disable, "true"); + this.cookies.all.forEach((key) => { + if (key.startsWith("_ga")) { + this.cookies.delete(key); + } + }); this.trackingEnabled = false; + window.location.reload(); } } }