From 249b9ecdc07da1929dfbe452054c3b05bcd5eaaa Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 24 Jan 2024 18:56:08 +0000 Subject: [PATCH 01/18] Initial idea for analytics package --- src/nationalarchives/analytics.mjs | 60 ++++++++++++++++++++++++++++++ webpack.config.js | 8 ++++ 2 files changed, 68 insertions(+) create mode 100644 src/nationalarchives/analytics.mjs diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs new file mode 100644 index 00000000..51796d42 --- /dev/null +++ b/src/nationalarchives/analytics.mjs @@ -0,0 +1,60 @@ +import Cookies from "./lib/cookies.mjs"; + +const valueGetters = { + text: (el) => el.innerText, + html: (el) => el.innerHTML, +}; + +const config = { + breadcrumbs: [ + { + onEvent: "click", + targetElement: + ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", + data: { event: "click", value: valueGetters.text }, + }, + { + onEvent: "click", + targetElement: + ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", + data: { event: "expand", value: valueGetters.html }, + }, + ], +}; + +class GoogleAnalytics4 { + /** @protected */ + cookies = new Cookies(); + + constructor(id) { + console.log(`BOOM! ${id}`); + console.log(this.cookies); + console.log(config); + + Object.keys(config).forEach((component) => { + config[component].forEach((componentTracking) => { + Array.from( + document.querySelectorAll(componentTracking.targetElement), + ).forEach((element) => { + const el = element; + let data = componentTracking.data; + el.addEventListener(componentTracking.onEvent, (event) => { + event.preventDefault(); + console.log(data.value); + console.log(data.value.call(this, el)); + data = { + ...data, + value: data.value.call(this, el), + }; + console.log(data); + }); + }); + }); + }); + } + + _addTrackingCode() {} + _removeTrackingCode() {} +} + +export { GoogleAnalytics4 }; diff --git a/webpack.config.js b/webpack.config.js index 6ad57889..b2d3046c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,6 +11,14 @@ module.exports = { type: "umd", }, }, + analytics: { + import: "./src/nationalarchives/analytics.mjs", + filename: "analytics.js", + library: { + name: "TNAFrontendAnalytics", + type: "umd", + }, + }, ...glob .sync("./src/nationalarchives/components/**/*.mjs") .reduce((acc, path) => { From a9f1e3b5e855efa7413b2e28950cfb2d70276594 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 25 Jan 2024 11:14:07 +0000 Subject: [PATCH 02/18] Add more analytics functionality --- src/nationalarchives/analytics.mjs | 116 +++++++++++++++++++++-------- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 51796d42..d2fcbec4 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -1,26 +1,56 @@ import Cookies from "./lib/cookies.mjs"; const valueGetters = { - text: (el) => el.innerText, - html: (el) => el.innerHTML, + text: ($el, $scope, event) => $el.innerText, + html: ($el, $scope, event) => $el.innerHTML, }; -const config = { - breadcrumbs: [ - { - onEvent: "click", - targetElement: - ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", - data: { event: "click", value: valueGetters.text }, +const config = [ + { + scope: ".tna-breadcrumbs", + data: { + name: "Breadcrumbs", }, - { - onEvent: "click", - targetElement: - ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", - data: { event: "expand", value: valueGetters.html }, + events: [ + { + onEvent: "click", + targetElement: + ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", + data: { event: "link", value: valueGetters.text }, + }, + { + onEvent: "click", + targetElement: + ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", + data: { event: "click", state: "expand", value: valueGetters.html }, + }, + ], + }, + { + scope: ".tna-picture", + data: { + name: "Picture", }, - ], -}; + events: [ + { + onEvent: "click", + targetElement: ".tna-picture__toggle-transcript", + data: { + event: "click", + state: ($el, $scope, event) => { + const expanded = $el.getAttribute("aria-expanded"); + if (expanded === null) { + return null; + } + return expanded.toString() === "true" ? "expanded" : "closed"; + }, + value: ($el, $scope, event) => + $scope.querySelector(".tna-picture__image").getAttribute("alt"), + }, + }, + ], + }, +]; class GoogleAnalytics4 { /** @protected */ @@ -28,25 +58,37 @@ class GoogleAnalytics4 { constructor(id) { console.log(`BOOM! ${id}`); - console.log(this.cookies); - console.log(config); - Object.keys(config).forEach((component) => { - config[component].forEach((componentTracking) => { + config.forEach((configScope) => { + this.addListener(configScope.scope, configScope.data, configScope.events); + }); + } + + addListener(scope, data, events) { + Array.from(document.querySelectorAll(scope)).forEach(($scope) => { + events.forEach((componentTracking) => { Array.from( - document.querySelectorAll(componentTracking.targetElement), + $scope.querySelectorAll(componentTracking.targetElement), ).forEach((element) => { - const el = element; - let data = componentTracking.data; - el.addEventListener(componentTracking.onEvent, (event) => { - event.preventDefault(); - console.log(data.value); - console.log(data.value.call(this, el)); - data = { - ...data, - value: data.value.call(this, el), + const $el = element; + const eventDataInit = { + ...data, + ...componentTracking.data, + }; + $el.addEventListener(componentTracking.onEvent, (event) => { + const eventData = { + ...eventDataInit, + value: + typeof eventDataInit.value === "function" + ? eventDataInit.value.call(this, $el, $scope, event) + : eventDataInit.value, + state: + typeof eventDataInit.state === "function" + ? eventDataInit.state.call(this, $el, $scope, event) + : eventDataInit.state, + timestamp: new Date().toISOString(), }; - console.log(data); + console.log(eventData); }); }); }); @@ -57,4 +99,16 @@ class GoogleAnalytics4 { _removeTrackingCode() {} } +const analytics = new GoogleAnalytics4("test"); +analytics.addListener(".etna-article__sidebar", { name: "Sidebar" }, [ + { + onEvent: "click", + targetElement: ".etna-article__sidebar-item", + data: { + event: "scection_jump", + value: valueGetters.text, + }, + }, +]); + export { GoogleAnalytics4 }; From 628911521c30f8c30d4f0cf10ade2e5140dc20f9 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 25 Jan 2024 14:05:03 +0000 Subject: [PATCH 03/18] Define per-component analytics --- src/nationalarchives/analytics.mjs | 218 +++++++++++------- .../components/breadcrumbs/analytics.js | 24 ++ .../components/header/analytics.js | 26 +++ .../components/hero/analytics.js | 28 +++ .../components/picture/analytics.js | 28 +++ .../lib/analytics-helpers.mjs | 30 +++ 6 files changed, 274 insertions(+), 80 deletions(-) create mode 100644 src/nationalarchives/components/breadcrumbs/analytics.js create mode 100644 src/nationalarchives/components/header/analytics.js create mode 100644 src/nationalarchives/components/hero/analytics.js create mode 100644 src/nationalarchives/components/picture/analytics.js create mode 100644 src/nationalarchives/lib/analytics-helpers.mjs diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index d2fcbec4..1e1d498c 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -1,114 +1,172 @@ import Cookies from "./lib/cookies.mjs"; +import { getXPathTo, valueGetters } from "./lib/analytics-helpers.mjs"; +import BreadcrumbAnalytics from "./components/breadcrumbs/analytics.js"; +import HeaderAnalytics from "./components/header/analytics.js"; +import HeroAnalytics from "./components/hero/analytics.js"; +import PictureAnalytics from "./components/picture/analytics.js"; -const valueGetters = { - text: ($el, $scope, event) => $el.innerText, - html: ($el, $scope, event) => $el.innerHTML, -}; - -const config = [ - { - scope: ".tna-breadcrumbs", - data: { - name: "Breadcrumbs", - }, - events: [ - { - onEvent: "click", - targetElement: - ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", - data: { event: "link", value: valueGetters.text }, - }, - { - onEvent: "click", - targetElement: - ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", - data: { event: "click", state: "expand", value: valueGetters.html }, - }, - ], - }, - { - scope: ".tna-picture", - data: { - name: "Picture", - }, - events: [ - { - onEvent: "click", - targetElement: ".tna-picture__toggle-transcript", - data: { - event: "click", - state: ($el, $scope, event) => { - const expanded = $el.getAttribute("aria-expanded"); - if (expanded === null) { - return null; - } - return expanded.toString() === "true" ? "expanded" : "closed"; - }, - value: ($el, $scope, event) => - $scope.querySelector(".tna-picture__image").getAttribute("alt"), - }, - }, - ], - }, +const componentAnalytics = [ + ...BreadcrumbAnalytics, + ...HeaderAnalytics, + ...HeroAnalytics, + ...PictureAnalytics, ]; -class GoogleAnalytics4 { +class EventTracker { /** @protected */ cookies = new Cookies(); - constructor(id) { - console.log(`BOOM! ${id}`); + /** @protected */ + events = []; - config.forEach((configScope) => { - this.addListener(configScope.scope, configScope.data, configScope.events); + constructor() { + componentAnalytics.forEach((component) => { + this.addListener(component.scope, component.data, component.events); }); } addListener(scope, data, events) { - Array.from(document.querySelectorAll(scope)).forEach(($scope) => { + let scopeArray; + if (typeof scope === "string") { + scopeArray = Array.from(document.querySelectorAll(scope)); + } else if (typeof scope === "object") { + scopeArray = [scope]; + } + if (!scopeArray) { + return; + } + scopeArray.forEach(($scope) => { events.forEach((componentTracking) => { - Array.from( - $scope.querySelectorAll(componentTracking.targetElement), - ).forEach((element) => { - const $el = element; - const eventDataInit = { + if (!componentTracking.onEvent) { + return; + } + + if (componentTracking.targetElement) { + Array.from( + $scope.querySelectorAll(componentTracking.targetElement), + ).forEach(($el) => + this.attachListener($el, componentTracking.onEvent, $scope, { + ...data, + ...componentTracking.data, + }), + ); + } else { + this.attachListener($scope, componentTracking.onEvent, $scope, { ...data, ...componentTracking.data, - }; - $el.addEventListener(componentTracking.onEvent, (event) => { - const eventData = { - ...eventDataInit, - value: - typeof eventDataInit.value === "function" - ? eventDataInit.value.call(this, $el, $scope, event) - : eventDataInit.value, - state: - typeof eventDataInit.state === "function" - ? eventDataInit.state.call(this, $el, $scope, event) - : eventDataInit.state, - timestamp: new Date().toISOString(), - }; - console.log(eventData); }); - }); + } }); }); } - _addTrackingCode() {} + attachListener($el, eventTrigger, $scope, eventDataInit) { + $el.addEventListener(eventTrigger, (event) => + this.recordEvent({ + ...eventDataInit, + value: + typeof eventDataInit.value === "function" + ? eventDataInit.value.call(this, $el, $scope, event) + : eventDataInit.value || null, + state: + typeof eventDataInit.state === "function" + ? eventDataInit.state.call(this, $el, $scope, event) + : eventDataInit.state || null, + }), + ); + } + + recordEvent(data) { + const expandedData = { + ...data, + timestamp: new Date().toISOString(), + uri: window.location.pathname, + }; + console.log(expandedData); + this.events.push(expandedData); + } +} + +class GoogleAnalytics4 extends EventTracker { + constructor(id) { + super(); + console.log(`Tracking ID: ${id}`); + } + + _addTrackingCode() { + const script = document.createElement("script"); + script.type = "text/javascript"; + script.src = ""; + document.head.appendChild(script); + } + _removeTrackingCode() {} } +/* + * ========================================== + * TEMP TESTING + * ========================================== + */ const analytics = new GoogleAnalytics4("test"); analytics.addListener(".etna-article__sidebar", { name: "Sidebar" }, [ { - onEvent: "click", targetElement: ".etna-article__sidebar-item", + onEvent: "click", data: { - event: "scection_jump", + eventName: "scection_jump", value: valueGetters.text, }, }, ]); +analytics.addListener(".etna-article", { name: "Article" }, [ + { + targetElement: ".etna-article__section-button", + onEvent: "click", + data: { + eventName: "scection_toggle", + state: ($el, $scope, event) => { + const expanded = $el.getAttribute("aria-expanded"); + if (expanded === null) { + return null; + } + return expanded.toString() === "true" ? "expanded" : "closed"; + }, + value: valueGetters.text, + }, + }, +]); +analytics.addListener(document, { name: "Document" }, [ + // { + // onEvent: "scroll", + // data: { + // eventName: "page_scroll", + // value: ($el, $scope, event) => $scope.querySelector("html").scrollTop, + // }, + // }, +]); +analytics.addListener(document.documentElement, { name: "HTML" }, [ + { + onEvent: "dblclick", + data: { + eventName: "double_click", + state: ($el, $scope, event) => getXPathTo(event.target), + value: ($el, $scope, event) => event.target.innerHTML, + }, + }, +]); +analytics.addListener( + document.getElementById("tna-form__search"), + { name: "Search input" }, + [ + { + onEvent: "blur", + data: { + eventName: "search_term_blur", + value: valueGetters.value, + }, + }, + ], +); export { GoogleAnalytics4 }; diff --git a/src/nationalarchives/components/breadcrumbs/analytics.js b/src/nationalarchives/components/breadcrumbs/analytics.js new file mode 100644 index 00000000..da7ae284 --- /dev/null +++ b/src/nationalarchives/components/breadcrumbs/analytics.js @@ -0,0 +1,24 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-breadcrumbs", + data: { + name: "Breadcrumbs", + }, + events: [ + { + targetElement: + ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", + onEvent: "click", + data: { eventName: "link", value: valueGetters.text }, + }, + { + targetElement: + ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", + onEvent: "click", + data: { eventName: "click", state: "expand", value: valueGetters.html }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/header/analytics.js b/src/nationalarchives/components/header/analytics.js new file mode 100644 index 00000000..e20655ec --- /dev/null +++ b/src/nationalarchives/components/header/analytics.js @@ -0,0 +1,26 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-header", + data: { + name: "Header", + }, + events: [ + { + targetElement: ".tna-header__navigation-toggle-button", + onEvent: "click", + data: { + eventName: "toggle", + state: ($el, $scope, event) => { + const expanded = $el.getAttribute("aria-expanded"); + if (expanded === null) { + return null; + } + return expanded.toString() === "true" ? "expanded" : "closed"; + }, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/hero/analytics.js b/src/nationalarchives/components/hero/analytics.js new file mode 100644 index 00000000..7ea24472 --- /dev/null +++ b/src/nationalarchives/components/hero/analytics.js @@ -0,0 +1,28 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-hero", + data: { + name: "Hero", + }, + events: [ + { + targetElement: ".tna-hero__details-summary", + onEvent: "click", + data: { + eventName: "click", + state: ($el, $scope, event) => { + const wasExpanded = + $scope + .querySelector(".tna-hero__details") + ?.hasAttribute("open") ?? false; + return wasExpanded ? "closed" : "expanded"; + }, + value: ($el, $scope, event) => + $scope.querySelector("img[alt]")?.getAttribute("alt"), + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/picture/analytics.js b/src/nationalarchives/components/picture/analytics.js new file mode 100644 index 00000000..7b1a1681 --- /dev/null +++ b/src/nationalarchives/components/picture/analytics.js @@ -0,0 +1,28 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-picture", + data: { + name: "Picture", + }, + events: [ + { + targetElement: ".tna-picture__toggle-transcript", + onEvent: "click", + data: { + eventName: "toggle", + state: ($el, $scope, event) => { + const expanded = $el.getAttribute("aria-expanded"); + if (expanded === null) { + return null; + } + return expanded.toString() === "true" ? "expanded" : "closed"; + }, + value: ($el, $scope, event) => + $scope.querySelector(".tna-picture__image").getAttribute("alt"), + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/lib/analytics-helpers.mjs b/src/nationalarchives/lib/analytics-helpers.mjs new file mode 100644 index 00000000..28b42e1f --- /dev/null +++ b/src/nationalarchives/lib/analytics-helpers.mjs @@ -0,0 +1,30 @@ +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 valueGetters = { + text: ($el, $scope, event) => $el.innerText, + html: ($el, $scope, event) => $el.innerHTML, + value: ($el, $scope, event) => $el.value, +}; + +export { getXPathTo, valueGetters }; From fe0cb799b3ec58c945f17b7cfc09c99fc1fd5ec5 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 25 Jan 2024 17:04:56 +0000 Subject: [PATCH 04/18] Add GA4 handling --- src/nationalarchives/analytics.mjs | 190 ++++++++++++------ .../components/breadcrumbs/analytics.js | 13 +- .../components/header/analytics.js | 9 +- .../components/hero/analytics.js | 10 +- .../components/picture/analytics.js | 10 +- .../lib/analytics-helpers.mjs | 3 + src/nationalarchives/lib/cookies.mjs | 94 +++++---- src/nationalarchives/tests/cookies.test.js | 21 ++ 8 files changed, 226 insertions(+), 124 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 1e1d498c..417b2d2c 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -14,18 +14,28 @@ const componentAnalytics = [ class EventTracker { /** @protected */ - cookies = new Cookies(); + cookies = new (window.TNAFrontend?.Cookies || Cookies)(); /** @protected */ events = []; constructor() { - componentAnalytics.forEach((component) => { - this.addListener(component.scope, component.data, component.events); + componentAnalytics.forEach((componentConfig) => { + this.addListener( + componentConfig.scope, + componentConfig.areaName, + componentConfig.events, + ); }); } - addListener(scope, data, events) { + /** + * 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, onEvent: 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. + */ + addListener(scope, areaName, events) { let scopeArray; if (typeof scope === "string") { scopeArray = Array.from(document.querySelectorAll(scope)); @@ -40,29 +50,41 @@ class EventTracker { if (!componentTracking.onEvent) { return; } - if (componentTracking.targetElement) { Array.from( $scope.querySelectorAll(componentTracking.targetElement), ).forEach(($el) => - this.attachListener($el, componentTracking.onEvent, $scope, { - ...data, - ...componentTracking.data, - }), + this.attachListener( + $el, + componentTracking.onEvent, + $scope, + this.generateEventName(areaName, componentTracking), + componentTracking.data, + ), ); } else { - this.attachListener($scope, componentTracking.onEvent, $scope, { - ...data, - ...componentTracking.data, - }); + this.attachListener( + $scope, + componentTracking.onEvent, + $scope, + this.generateEventName(areaName, componentTracking), + componentTracking.data, + ); } }); }); } - attachListener($el, eventTrigger, $scope, eventDataInit) { + generateEventName(areaName, componentTracking) { + return `${areaName}.${ + componentTracking.eventName || componentTracking.onEvent + }`; + } + + /** @protected */ + attachListener($el, eventTrigger, $scope, eventName, eventDataInit) { $el.addEventListener(eventTrigger, (event) => - this.recordEvent({ + this.recordEvent(eventName, { ...eventDataInit, value: typeof eventDataInit.value === "function" @@ -72,35 +94,89 @@ class EventTracker { typeof eventDataInit.state === "function" ? eventDataInit.state.call(this, $el, $scope, event) : eventDataInit.state || null, + timestamp: new Date().toISOString(), + uri: window.location.pathname, }), ); } - recordEvent(data) { - const expandedData = { - ...data, - timestamp: new Date().toISOString(), - uri: window.location.pathname, - }; - console.log(expandedData); - this.events.push(expandedData); + /** @protected */ + recordEvent(eventName, data) { + this.events.push({ event: eventName, data }); } } -class GoogleAnalytics4 extends EventTracker { +/** + * Class to handle Google Analytics 4 reporting. + * @class GA4 + * @extends EventTracker + * @constructor + * @public + */ +class GA4 extends EventTracker { + trackingCodeAdded = false; + trackingEnabled = false; + gTagId; + constructor(id) { super(); - console.log(`Tracking ID: ${id}`); + this.gTagId = id; + 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(); + } + } + }); + } + + /** @protected */ + recordEvent(eventName, data) { + const ga4Data = { event: eventName, data }; + window.dataLayer.push(ga4Data); + console.log(window.dataLayer); } - _addTrackingCode() { - const script = document.createElement("script"); - script.type = "text/javascript"; - script.src = ""; - document.head.appendChild(script); + /** @protected */ + gtag() { + window.dataLayer.push(arguments); + console.log(window.dataLayer); + } + + /** @protected */ + enableTracking() { + if (!this.trackingEnabled) { + window["ga-disable-GA_MEASUREMENT_ID"] = false; + this.trackingEnabled = true; + if (!this.trackingCodeAdded) { + const script = document.createElement("script"); + script.setAttribute("async", true); + script.setAttribute( + "src", + `https://www.googletagmanager.com/gtm.js?id=${this.gTagId}&l=dataLayer`, + ); + // document.head.appendChild(script); + this.gtag("js", new Date()); + this.trackingCodeAdded = true; + } + this.gtag("set", { allow_ad_personalization_signals: false }); + } } - _removeTrackingCode() {} + /** @protected */ + disableTracking() { + if (this.trackingEnabled) { + window["ga-disable-GA_MEASUREMENT_ID"] = true; + this.gtag("set", { allow_ad_personalization_signals: true }); + this.trackingEnabled = false; + } + } } /* @@ -108,23 +184,24 @@ class GoogleAnalytics4 extends EventTracker { * TEMP TESTING * ========================================== */ -const analytics = new GoogleAnalytics4("test"); -analytics.addListener(".etna-article__sidebar", { name: "Sidebar" }, [ +const analytics = new GA4("test"); +analytics.addListener(".etna-article__sidebar", "sidebar", [ { + eventName: "scection_jump", targetElement: ".etna-article__sidebar-item", onEvent: "click", data: { - eventName: "scection_jump", value: valueGetters.text, }, }, ]); -analytics.addListener(".etna-article", { name: "Article" }, [ +analytics.addListener(".etna-article", "article", [ { + eventName: "scection_toggle", targetElement: ".etna-article__section-button", onEvent: "click", data: { - eventName: "scection_toggle", + // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { const expanded = $el.getAttribute("aria-expanded"); if (expanded === null) { @@ -136,37 +213,28 @@ analytics.addListener(".etna-article", { name: "Article" }, [ }, }, ]); -analytics.addListener(document, { name: "Document" }, [ - // { - // onEvent: "scroll", - // data: { - // eventName: "page_scroll", - // value: ($el, $scope, event) => $scope.querySelector("html").scrollTop, - // }, - // }, -]); -analytics.addListener(document.documentElement, { name: "HTML" }, [ +analytics.addListener(document.documentElement, "doc", [ { + eventName: "double_click", onEvent: "dblclick", data: { - eventName: "double_click", state: ($el, $scope, event) => getXPathTo(event.target), value: ($el, $scope, event) => event.target.innerHTML, }, }, ]); -analytics.addListener( - document.getElementById("tna-form__search"), - { name: "Search input" }, - [ - { - onEvent: "blur", - data: { - eventName: "search_term_blur", - value: valueGetters.value, - }, - }, - ], -); +// analytics.addListener( +// document.getElementById("tna-form__search"), +// "search", +// [ +// { +// eventName: "search_term_blur", +// onEvent: "blur", +// data: { +// value: valueGetters.value, +// }, +// }, +// ], +// ); -export { GoogleAnalytics4 }; +export { EventTracker, GA4 }; diff --git a/src/nationalarchives/components/breadcrumbs/analytics.js b/src/nationalarchives/components/breadcrumbs/analytics.js index da7ae284..a0ea1b76 100644 --- a/src/nationalarchives/components/breadcrumbs/analytics.js +++ b/src/nationalarchives/components/breadcrumbs/analytics.js @@ -3,21 +3,14 @@ import { valueGetters } from "../../lib/analytics-helpers.mjs"; export default [ { scope: ".tna-breadcrumbs", - data: { - name: "Breadcrumbs", - }, + areaName: "breadcrumbs", events: [ { - targetElement: - ".tna-breadcrumbs__item:not(.tna-breadcrumbs__item--expandable) .tna-breadcrumbs__link", - onEvent: "click", - data: { eventName: "link", value: valueGetters.text }, - }, - { + eventName: "click", targetElement: ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", onEvent: "click", - data: { eventName: "click", state: "expand", value: valueGetters.html }, + data: { state: "expand", value: valueGetters.html }, }, ], }, diff --git a/src/nationalarchives/components/header/analytics.js b/src/nationalarchives/components/header/analytics.js index e20655ec..223b2a69 100644 --- a/src/nationalarchives/components/header/analytics.js +++ b/src/nationalarchives/components/header/analytics.js @@ -1,17 +1,14 @@ -import { valueGetters } from "../../lib/analytics-helpers.mjs"; - export default [ { scope: ".tna-header", - data: { - name: "Header", - }, + areaName: "header", events: [ { + eventName: "toggle", targetElement: ".tna-header__navigation-toggle-button", onEvent: "click", data: { - eventName: "toggle", + // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { const expanded = $el.getAttribute("aria-expanded"); if (expanded === null) { diff --git a/src/nationalarchives/components/hero/analytics.js b/src/nationalarchives/components/hero/analytics.js index 7ea24472..8627e8ef 100644 --- a/src/nationalarchives/components/hero/analytics.js +++ b/src/nationalarchives/components/hero/analytics.js @@ -1,17 +1,14 @@ -import { valueGetters } from "../../lib/analytics-helpers.mjs"; - export default [ { scope: ".tna-hero", - data: { - name: "Hero", - }, + areaName: "hero", events: [ { + eventName: "toggle", targetElement: ".tna-hero__details-summary", onEvent: "click", data: { - eventName: "click", + // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { const wasExpanded = $scope @@ -19,6 +16,7 @@ export default [ ?.hasAttribute("open") ?? false; return wasExpanded ? "closed" : "expanded"; }, + // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => $scope.querySelector("img[alt]")?.getAttribute("alt"), }, diff --git a/src/nationalarchives/components/picture/analytics.js b/src/nationalarchives/components/picture/analytics.js index 7b1a1681..e54f6461 100644 --- a/src/nationalarchives/components/picture/analytics.js +++ b/src/nationalarchives/components/picture/analytics.js @@ -1,17 +1,14 @@ -import { valueGetters } from "../../lib/analytics-helpers.mjs"; - export default [ { scope: ".tna-picture", - data: { - name: "Picture", - }, + areaName: "picture", events: [ { + eventName: "toggle", targetElement: ".tna-picture__toggle-transcript", onEvent: "click", data: { - eventName: "toggle", + // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { const expanded = $el.getAttribute("aria-expanded"); if (expanded === null) { @@ -19,6 +16,7 @@ export default [ } return expanded.toString() === "true" ? "expanded" : "closed"; }, + // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => $scope.querySelector(".tna-picture__image").getAttribute("alt"), }, diff --git a/src/nationalarchives/lib/analytics-helpers.mjs b/src/nationalarchives/lib/analytics-helpers.mjs index 28b42e1f..7d04cb93 100644 --- a/src/nationalarchives/lib/analytics-helpers.mjs +++ b/src/nationalarchives/lib/analytics-helpers.mjs @@ -22,8 +22,11 @@ const getXPathTo = (element) => { } }; const valueGetters = { + // eslint-disable-next-line no-unused-vars text: ($el, $scope, event) => $el.innerText, + // eslint-disable-next-line no-unused-vars html: ($el, $scope, event) => $el.innerHTML, + // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => $el.value, }; diff --git a/src/nationalarchives/lib/cookies.mjs b/src/nationalarchives/lib/cookies.mjs index 05757afc..28acf533 100644 --- a/src/nationalarchives/lib/cookies.mjs +++ b/src/nationalarchives/lib/cookies.mjs @@ -1,5 +1,6 @@ export class CookieEventHandler { events = {}; + oneTimeEvents = {}; constructor() { if (CookieEventHandler._instance) { @@ -10,23 +11,37 @@ export class CookieEventHandler { /** * Add an event listener. - * @param {string} event - The event to add a listener for. - * @param {function} callback - The callback function to call when the event is triggered. + * @param {String} event - The event to add a listener for. + * @param {Function} callback - The callback function to call when the event is triggered. */ on(event, callback) { - if (!Object.prototype.hasOwnProperty.call(this.events, event)) { + if (!Object.hasOwn(this.events, event)) { this.events[event] = []; } this.events[event] = [...this.events[event], callback]; } + once(event, callback) { + if (!Object.hasOwn(this.oneTimeEvents, event)) { + this.oneTimeEvents[event] = []; + } + this.oneTimeEvents[event] = [...this.oneTimeEvents[event], callback]; + } + /** @protected */ trigger(event, data = {}) { - if (Object.prototype.hasOwnProperty.call(this.events, event)) { + if (Object.hasOwn(this.events, event)) { this.events[event].forEach((eventToTrigger) => eventToTrigger.call(this, data), ); } + if (Object.hasOwn(this.oneTimeEvents, event)) { + for (let i = this.oneTimeEvents[event].length - 1; i >= 0; i--) { + const eventToTrigger = this.oneTimeEvents[event][i]; + eventToTrigger.call(this, data); + this.oneTimeEvents[event].splice(i, 1); + } + } } } @@ -50,11 +65,11 @@ export default class Cookies { /** * Create a cookie handler. - * @param {string} [options.extraPolicies=[]] - The extra cookie policies to manage in addition to essential, settings and usage. - * @param {string} [options.domain=""] - The domain to register the cookie with. - * @param {string} [options.path=""] - The domain to register the cookie with. - * @param {string} [options.secure=true] - Only set cookie in HTTPS environments. - * @param {string} [options.policiesKey=cookies_policy] - The name of the cookie. + * @param {String} [options.extraPolicies=[]] - The extra cookie policies to manage in addition to essential, settings and usage. + * @param {String} [options.domain=""] - The domain to register the cookie with. + * @param {String} [options.path=""] - The domain to register the cookie with. + * @param {String} [options.secure=true] - Only set cookie in HTTPS environments. + * @param {String} [options.policiesKey=cookies_policy] - The name of the cookie. */ constructor(options = {}) { const { @@ -110,17 +125,17 @@ export default class Cookies { /** * Check to see whether a cookie exists or not. - * @param {string} key - The cookie name. - * @returns {boolean} + * @param {String} key - The cookie name. + * @returns {Boolean} */ exists(key) { - return Object.prototype.hasOwnProperty.call(this.all, key); + return Object.hasOwn(this.all, key); } /** * Check to see whether a cookie has a particular value. - * @param {string} key - The cookie name. - * @param {string|number|boolean} value - The value to check against. + * @param {String} key - The cookie name. + * @param {String|Number|Boolean} value - The value to check against. * @returns */ hasValue(key, value) { @@ -129,8 +144,8 @@ export default class Cookies { /** * Get a cookie. - * @param {string} key - The cookie name. - * @returns {string|number|boolean} + * @param {String} key - The cookie name. + * @returns {String|Number|Boolean} */ get(key) { return this.exists(key) ? decodeURIComponent(this.all[key]) : null; @@ -138,15 +153,15 @@ export default class Cookies { /** * Set a cookie. - * @param {string} key - The cookie name. - * @param {string|number|boolean} value - The cookie value. + * @param {String} key - The cookie name. + * @param {String|Number|Boolean} value - The cookie value. * @param {Object} options - * @param {number} [options.maxAge=31536000] - The maximum age of the cookie in seconds. - * @param {string} [options.path=/] - The path to register the cookie for. - * @param {string} [options.sameSite=Lax] - The sameSite attribute. - * @param {string} [options.domain=this.domain] - The domain to register the cookie with. - * @param {string} [options.path=this.path] - The path to register the cookie with. - * @param {string} [options.secure=this.secure] - Only set cookie in HTTPS environments. + * @param {Number} [options.maxAge=31536000] - The maximum age of the cookie in seconds. + * @param {String} [options.path=/] - The path to register the cookie for. + * @param {String} [options.sameSite=Lax] - The sameSite attribute. + * @param {String} [options.domain=this.domain] - The domain to register the cookie with. + * @param {String} [options.path=this.path] - The path to register the cookie with. + * @param {String} [options.secure=this.secure] - Only set cookie in HTTPS environments. */ set(key, value, options = {}) { const { @@ -179,8 +194,8 @@ export default class Cookies { /** * Delete a cookie. - * @param {string} key - The cookie name. - * @param {string} [path=/] - The path to the cookie is registered on. + * @param {String} key - The cookie name. + * @param {String} [path=/] - The path to the cookie is registered on. */ delete(key, path = "/", domain = null) { const options = { maxAge: -1, path, domain: domain || undefined }; @@ -200,7 +215,7 @@ export default class Cookies { /** * Accept a policy. - * @param {string} policy - The name of the policy. + * @param {String} policy - The name of the policy. */ acceptPolicy(policy) { this.setPolicy(policy, true); @@ -210,7 +225,7 @@ export default class Cookies { /** * Reject a policy. - * @param {string} policy - The name of the policy. + * @param {String} policy - The name of the policy. */ rejectPolicy(policy) { this.setPolicy(policy, false); @@ -220,8 +235,8 @@ export default class Cookies { /** * Set a policy. - * @param {string} policy - The name of the policy. - * @param {boolean} accepted - Whether the policy is accepted or not. + * @param {String} policy - The name of the policy. + * @param {Boolean} accepted - Whether the policy is accepted or not. */ setPolicy(policy, accepted) { if (policy === "essential") { @@ -272,21 +287,30 @@ export default class Cookies { /** * Get the acceptance status of a policy. - * @param {string} policy - The name of the policy. - * @returns {boolean} + * @param {String} policy - The name of the policy. + * @returns {Boolean} */ isPolicyAccepted(policy) { - return Object.prototype.hasOwnProperty.call(this.policies, policy) + return Object.hasOwn(this.policies, policy) ? this.policies[policy] === true : null; } /** * Add an event listener. - * @param {string} event - The event to add a listener for. - * @param {function} callback - The callback function to call when the event is triggered. + * @param {String} event - The event to add a listener for. + * @param {Function} callback - The callback function to call when the event is triggered. */ on(event, callback) { this.events.on(event, callback); } + + /** + * Add a one-time event listener. + * @param {String} event - The event to add a listener for. + * @param {Function} callback - The callback function to call when the event is triggered. + */ + once(event, callback) { + this.events.once(event, callback); + } } diff --git a/src/nationalarchives/tests/cookies.test.js b/src/nationalarchives/tests/cookies.test.js index 0c41cf07..951f7a09 100644 --- a/src/nationalarchives/tests/cookies.test.js +++ b/src/nationalarchives/tests/cookies.test.js @@ -347,6 +347,8 @@ describe("No existing cookies", () => { const testKey = "foo"; const testValue = "bar"; + expect(mockCallback.mock.calls).toHaveLength(0); + cookies1.set(testKey, testValue); expect(mockCallback.mock.calls).toHaveLength(1); @@ -357,6 +359,25 @@ describe("No existing cookies", () => { expect(mockCallback.mock.calls).toHaveLength(3); }); + test("One-time events", async () => { + const mockCallback = jest.fn(); + + const cookies = new Cookies(); + + cookies.once("setCookie", mockCallback); + + const testKey = "foo"; + const testValue = "bar"; + + expect(mockCallback.mock.calls).toHaveLength(0); + + cookies.set(testKey, testValue); + expect(mockCallback.mock.calls).toHaveLength(1); + + cookies.set(testKey, testValue); + expect(mockCallback.mock.calls).toHaveLength(1); + }); + test("Custom policies", async () => { const cookies = new Cookies({ extraPolicies: ["custom"] }); From 7359b15e11f483afdcf6fb92cc35008cd581daee Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 25 Jan 2024 17:55:24 +0000 Subject: [PATCH 05/18] More resiliance for missing elements, time since load, test skeleton --- src/nationalarchives/analytics.mjs | 33 +++++++++++--------- src/nationalarchives/tests/analytics.test.js | 11 +++++++ src/nationalarchives/tests/uuid.test.js | 6 ---- 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 src/nationalarchives/tests/analytics.test.js diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 417b2d2c..7cd4b782 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -19,6 +19,9 @@ class EventTracker { /** @protected */ events = []; + /** @protected */ + start = new Date(); + constructor() { componentAnalytics.forEach((componentConfig) => { this.addListener( @@ -83,6 +86,9 @@ class EventTracker { /** @protected */ attachListener($el, eventTrigger, $scope, eventName, eventDataInit) { + if (!$el) { + return; + } $el.addEventListener(eventTrigger, (event) => this.recordEvent(eventName, { ...eventDataInit, @@ -96,6 +102,7 @@ class EventTracker { : eventDataInit.state || null, timestamp: new Date().toISOString(), uri: window.location.pathname, + timeSincePageLoad: new Date() - this.start, }), ); } @@ -140,13 +147,13 @@ class GA4 extends EventTracker { recordEvent(eventName, data) { const ga4Data = { event: eventName, data }; window.dataLayer.push(ga4Data); - console.log(window.dataLayer); + // console.log(window.dataLayer); } /** @protected */ gtag() { window.dataLayer.push(arguments); - console.log(window.dataLayer); + // console.log(window.dataLayer); } /** @protected */ @@ -223,18 +230,14 @@ analytics.addListener(document.documentElement, "doc", [ }, }, ]); -// analytics.addListener( -// document.getElementById("tna-form__search"), -// "search", -// [ -// { -// eventName: "search_term_blur", -// onEvent: "blur", -// data: { -// value: valueGetters.value, -// }, -// }, -// ], -// ); +analytics.addListener(document.getElementById("tna-form__search"), "search", [ + { + eventName: "search_term_blur", + onEvent: "blur", + data: { + value: valueGetters.value, + }, + }, +]); export { EventTracker, GA4 }; diff --git a/src/nationalarchives/tests/analytics.test.js b/src/nationalarchives/tests/analytics.test.js new file mode 100644 index 00000000..f77c5578 --- /dev/null +++ b/src/nationalarchives/tests/analytics.test.js @@ -0,0 +1,11 @@ +import { describe, expect, test } from "@jest/globals"; +import { GA4 } from "../analytics.mjs"; + +describe("Analytics", () => { + test("Initialisation", async () => { + const id = "example-id"; + const ga4 = new GA4(id); + + expect(ga4.gTagId).toEqual(id); + }); +}); diff --git a/src/nationalarchives/tests/uuid.test.js b/src/nationalarchives/tests/uuid.test.js index 70bd3e5b..8674481a 100644 --- a/src/nationalarchives/tests/uuid.test.js +++ b/src/nationalarchives/tests/uuid.test.js @@ -1,12 +1,6 @@ import { describe, expect, test } from "@jest/globals"; -import { TextEncoder, TextDecoder, store, options } from "util"; import uuidv4 from "../lib/uuid.mjs"; -global.TextEncoder = TextEncoder; -global.TextDecoder = TextDecoder; -global.store = store; -global.options = options; - describe("UUID", () => { test("Initialisation", async () => { const uuid1 = uuidv4(); From dd405141b4937b9154eab06c6ce613d77b0774cf Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 7 Feb 2024 17:03:24 +0000 Subject: [PATCH 06/18] Change HTML fixtures test location --- .github/actions/tests/action.yml | 2 +- .gitignore | 2 +- package.json | 2 +- tasks/generate-fixture-html.js | 7 ++++++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/actions/tests/action.yml b/.github/actions/tests/action.yml index 567a6a31..59895d4b 100644 --- a/.github/actions/tests/action.yml +++ b/.github/actions/tests/action.yml @@ -14,7 +14,7 @@ runs: run: npm ci shell: bash - name: Validate HTML - run: mkdir temp && npm run validatehtml + run: npm run validatehtml shell: bash - name: Build Storybook run: npm run build --test diff --git a/.gitignore b/.gitignore index d7924cad..3f30f5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ build-storybook.log chromatic.config.json chromatic.log RELEASE_NOTES.txt -temp \ No newline at end of file +fixtures-html \ No newline at end of file diff --git a/package.json b/package.json index 2c5e5f23..38fe790e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "lint:fix": "prettier --write '{src,.storybook,tasks,.}/**/*.{js,mjs,scss,json,html}' && stylelint --fix 'src/**/*.scss' && eslint --fix 'src/**/*.{js,mjs}'", "package:sass": "sass --style=compressed --embed-sources src/nationalarchives:package/nationalarchives", "package:scripts": "webpack", - "validatehtml": "node tasks/generate-fixture-html.js && html-validate temp" + "validatehtml": "node tasks/generate-fixture-html.js && html-validate fixtures-html" }, "repository": { "type": "git", diff --git a/tasks/generate-fixture-html.js b/tasks/generate-fixture-html.js index 37ff3fe1..914600cd 100644 --- a/tasks/generate-fixture-html.js +++ b/tasks/generate-fixture-html.js @@ -6,6 +6,11 @@ const { renderNunjucks } = require("./lib/nunjucks"); const componentsDirectory = "src/nationalarchives/components/"; const componentFixturesFile = "/fixtures.json"; +const fixturesOutputDirectory = "fixtures-html"; + +if (!fs.existsSync(fixturesOutputDirectory)) { + fs.mkdirSync(fixturesOutputDirectory); +} const components = globSync( `${componentsDirectory}*${componentFixturesFile}`, @@ -25,7 +30,7 @@ components.forEach((component) => { componentFixtures.fixtures.forEach((fixture) => { const result = renderNunjucks(componentNunjucks, fixture.options); fs.writeFile( - `temp/${component}-${fixture.name + `${fixturesOutputDirectory}/${component}-${fixture.name .replace(/[^0-9a-z]/gi, "-") .toLowerCase()}.html`, result, From 0e063c00e4491c106ab715b5a8b6e87ada1e5399 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 7 Feb 2024 18:02:50 +0000 Subject: [PATCH 07/18] Add more information to picture caption clicks --- src/nationalarchives/analytics.mjs | 40 ++++++++++++------- .../components/breadcrumbs/analytics.js | 2 +- .../components/header/analytics.js | 4 +- .../components/hero/analytics.js | 6 +-- .../components/picture/analytics.js | 40 ++++++++++++++++--- 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 7cd4b782..4a860836 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -36,7 +36,7 @@ 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, onEvent: 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, [String]: any}}[]} events - The configuration of events to track along with their optional value and state which can be computed. */ addListener(scope, areaName, events) { let scopeArray; @@ -50,7 +50,7 @@ class EventTracker { } scopeArray.forEach(($scope) => { events.forEach((componentTracking) => { - if (!componentTracking.onEvent) { + if (!componentTracking.on) { return; } if (componentTracking.targetElement) { @@ -59,19 +59,21 @@ class EventTracker { ).forEach(($el) => this.attachListener( $el, - componentTracking.onEvent, + componentTracking.on, $scope, this.generateEventName(areaName, componentTracking), componentTracking.data, + componentTracking.targetElement, ), ); } else { this.attachListener( $scope, - componentTracking.onEvent, + componentTracking.on, $scope, this.generateEventName(areaName, componentTracking), componentTracking.data, + componentTracking.targetElement, ); } }); @@ -79,13 +81,18 @@ class EventTracker { } generateEventName(areaName, componentTracking) { - return `${areaName}.${ - componentTracking.eventName || componentTracking.onEvent - }`; + return `${areaName}.${componentTracking.eventName || componentTracking.on}`; } /** @protected */ - attachListener($el, eventTrigger, $scope, eventName, eventDataInit) { + attachListener( + $el, + eventTrigger, + $scope, + eventName, + eventDataInit, + targetElement, + ) { if (!$el) { return; } @@ -100,6 +107,8 @@ class EventTracker { typeof eventDataInit.state === "function" ? eventDataInit.state.call(this, $el, $scope, event) : eventDataInit.state || null, + scope: getXPathTo($scope), + targetElement: targetElement, timestamp: new Date().toISOString(), uri: window.location.pathname, timeSincePageLoad: new Date() - this.start, @@ -192,21 +201,23 @@ class GA4 extends EventTracker { * ========================================== */ const analytics = new GA4("test"); + analytics.addListener(".etna-article__sidebar", "sidebar", [ { eventName: "scection_jump", targetElement: ".etna-article__sidebar-item", - onEvent: "click", + on: "click", data: { value: valueGetters.text, }, }, ]); + analytics.addListener(".etna-article", "article", [ { - eventName: "scection_toggle", + eventName: "toggle_section", targetElement: ".etna-article__section-button", - onEvent: "click", + on: "click", data: { // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { @@ -214,16 +225,17 @@ analytics.addListener(".etna-article", "article", [ if (expanded === null) { return null; } - return expanded.toString() === "true" ? "expanded" : "closed"; + return expanded.toString() === "true" ? "opened" : "closed"; }, value: valueGetters.text, }, }, ]); + analytics.addListener(document.documentElement, "doc", [ { eventName: "double_click", - onEvent: "dblclick", + on: "dblclick", data: { state: ($el, $scope, event) => getXPathTo(event.target), value: ($el, $scope, event) => event.target.innerHTML, @@ -233,7 +245,7 @@ analytics.addListener(document.documentElement, "doc", [ analytics.addListener(document.getElementById("tna-form__search"), "search", [ { eventName: "search_term_blur", - onEvent: "blur", + on: "blur", data: { value: valueGetters.value, }, diff --git a/src/nationalarchives/components/breadcrumbs/analytics.js b/src/nationalarchives/components/breadcrumbs/analytics.js index a0ea1b76..bf27ffa8 100644 --- a/src/nationalarchives/components/breadcrumbs/analytics.js +++ b/src/nationalarchives/components/breadcrumbs/analytics.js @@ -9,7 +9,7 @@ export default [ eventName: "click", targetElement: ".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link", - onEvent: "click", + on: "click", data: { state: "expand", value: valueGetters.html }, }, ], diff --git a/src/nationalarchives/components/header/analytics.js b/src/nationalarchives/components/header/analytics.js index 223b2a69..09630ae3 100644 --- a/src/nationalarchives/components/header/analytics.js +++ b/src/nationalarchives/components/header/analytics.js @@ -6,7 +6,7 @@ export default [ { eventName: "toggle", targetElement: ".tna-header__navigation-toggle-button", - onEvent: "click", + on: "click", data: { // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { @@ -14,7 +14,7 @@ export default [ if (expanded === null) { return null; } - return expanded.toString() === "true" ? "expanded" : "closed"; + return expanded.toString() === "true" ? "opened" : "closed"; }, }, }, diff --git a/src/nationalarchives/components/hero/analytics.js b/src/nationalarchives/components/hero/analytics.js index 8627e8ef..dc977618 100644 --- a/src/nationalarchives/components/hero/analytics.js +++ b/src/nationalarchives/components/hero/analytics.js @@ -4,9 +4,9 @@ export default [ areaName: "hero", events: [ { - eventName: "toggle", + eventName: "toggle_caption", targetElement: ".tna-hero__details-summary", - onEvent: "click", + on: "click", data: { // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { @@ -14,7 +14,7 @@ export default [ $scope .querySelector(".tna-hero__details") ?.hasAttribute("open") ?? false; - return wasExpanded ? "closed" : "expanded"; + return wasExpanded ? "closed" : "opened"; }, // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => diff --git a/src/nationalarchives/components/picture/analytics.js b/src/nationalarchives/components/picture/analytics.js index e54f6461..c20e9217 100644 --- a/src/nationalarchives/components/picture/analytics.js +++ b/src/nationalarchives/components/picture/analytics.js @@ -4,9 +4,9 @@ export default [ areaName: "picture", events: [ { - eventName: "toggle", + eventName: "toggle_transcript", targetElement: ".tna-picture__toggle-transcript", - onEvent: "click", + on: "click", data: { // eslint-disable-next-line no-unused-vars state: ($el, $scope, event) => { @@ -14,11 +14,41 @@ export default [ if (expanded === null) { return null; } - return expanded.toString() === "true" ? "expanded" : "closed"; + return expanded.toString() === "true" ? "opened" : "closed"; }, // eslint-disable-next-line no-unused-vars - value: ($el, $scope, event) => - $scope.querySelector(".tna-picture__image").getAttribute("alt"), + value: ($el, $scope, event) => { + const includesAny = (arr, values) => + values.some((v) => arr.includes(v)); + let heading = ""; + let $search = $scope; + 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); + const image = $scope.querySelector(".tna-picture__image"); + const imageSrc = image.getAttribute("src"); + const imageAlt = image.getAttribute("alt"); + const value = `${imageAlt} (${imageSrc})`; + return heading ? `${heading} > ${value}` : value; + }, }, }, ], From 9d93286c0a8b6e7e52201f85d3baac5a26baed32 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 7 Feb 2024 18:11:19 +0000 Subject: [PATCH 08/18] Abstract getClosestHeading --- .../components/picture/analytics.js | 28 ++--------- .../lib/analytics-helpers.mjs | 50 +++++++++++++++---- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/nationalarchives/components/picture/analytics.js b/src/nationalarchives/components/picture/analytics.js index c20e9217..a98ec088 100644 --- a/src/nationalarchives/components/picture/analytics.js +++ b/src/nationalarchives/components/picture/analytics.js @@ -1,3 +1,5 @@ +import { getClosestHeading } from "../../lib/analytics-helpers.mjs"; + export default [ { scope: ".tna-picture", @@ -18,31 +20,7 @@ export default [ }, // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => { - const includesAny = (arr, values) => - values.some((v) => arr.includes(v)); - let heading = ""; - let $search = $scope; - 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); + const heading = getClosestHeading($scope); const image = $scope.querySelector(".tna-picture__image"); const imageSrc = image.getAttribute("src"); const imageAlt = image.getAttribute("alt"); diff --git a/src/nationalarchives/lib/analytics-helpers.mjs b/src/nationalarchives/lib/analytics-helpers.mjs index 7d04cb93..cb839839 100644 --- a/src/nationalarchives/lib/analytics-helpers.mjs +++ b/src/nationalarchives/lib/analytics-helpers.mjs @@ -1,26 +1,54 @@ -const getXPathTo = (element) => { - if (element.id !== "") { - return 'id("' + element.id + '")'; +const getXPathTo = ($element) => { + if ($element.id !== "") { + return 'id("' + $element.id + '")'; } - if (element === document.body) { - return element.tagName; + if ($element === document.body) { + return $element.tagName; } let ix = 0; - const siblings = element.parentNode.childNodes; + const siblings = $element.parentNode.childNodes; for (let i = 0; i < siblings.length; i++) { const sibling = siblings[i]; - if (sibling === element) + if (sibling === $element) return ( - getXPathTo(element.parentNode) + + getXPathTo($element.parentNode) + "/" + - element.tagName + + $element.tagName + "[" + (ix + 1) + "]" ); - if (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++; + 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 = { // eslint-disable-next-line no-unused-vars text: ($el, $scope, event) => $el.innerText, @@ -30,4 +58,4 @@ const valueGetters = { value: ($el, $scope, event) => $el.value, }; -export { getXPathTo, valueGetters }; +export { getXPathTo, getClosestHeading, valueGetters }; From 1df18803d8a0376062df683b111f260873b305da Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 8 Feb 2024 12:38:22 +0000 Subject: [PATCH 09/18] Remove trailing slash on some
elements --- src/nationalarchives/analytics.mjs | 1 + src/nationalarchives/components/footer/template.njk | 2 +- .../stories/utilities/colour-schemes/colour-themes.stories.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 4a860836..58bd65ad 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -242,6 +242,7 @@ analytics.addListener(document.documentElement, "doc", [ }, }, ]); + analytics.addListener(document.getElementById("tna-form__search"), "search", [ { eventName: "search_term_blur", diff --git a/src/nationalarchives/components/footer/template.njk b/src/nationalarchives/components/footer/template.njk index 51236bdb..fb7a14cc 100644 --- a/src/nationalarchives/components/footer/template.njk +++ b/src/nationalarchives/components/footer/template.njk @@ -13,7 +13,7 @@ {%- if params.meta -%} diff --git a/src/nationalarchives/stories/utilities/colour-schemes/colour-themes.stories.js b/src/nationalarchives/stories/utilities/colour-schemes/colour-themes.stories.js index 0625364b..394481f9 100644 --- a/src/nationalarchives/stories/utilities/colour-schemes/colour-themes.stories.js +++ b/src/nationalarchives/stories/utilities/colour-schemes/colour-themes.stories.js @@ -1175,7 +1175,7 @@ const Template = ({ theme, accent }) => { ${Footer({ params: { - meta: "

Open today
09:00—19:00

", + meta: "

Open today
09:00—19:00

", social: [ { text: "Twitter", From e514e1e8e036093e1bb63c19857f4f7f07b3e367 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 8 Feb 2024 13:08:10 +0000 Subject: [PATCH 10/18] Get meta tags from HTML --- src/nationalarchives/analytics.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 58bd65ad..616a11bb 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -120,6 +120,25 @@ class EventTracker { recordEvent(eventName, data) { this.events.push({ event: eventName, data }); } + + getTnaMetaTags() { + // return Object.fromEntries( + // Array.from( + // document.head.querySelectorAll("meta[name^='tna:'][content]"), + // ).map(($metaEl) => [ + // metaData[$metaEl.getAttribute("name").replace(/^tna:/, "")], + // $metaEl.getAttribute("content"), + // ]), + // ); + const metaData = {}; + Array.from( + document.head.querySelectorAll("meta[name^='tna:'][content]"), + ).forEach(($metaEl) => { + metaData[$metaEl.getAttribute("name").replace(/^tna:/, "")] = + $metaEl.getAttribute("content"); + }); + return metaData; + } } /** @@ -180,6 +199,7 @@ class GA4 extends EventTracker { // document.head.appendChild(script); this.gtag("js", new Date()); this.trackingCodeAdded = true; + window.dataLayer.push(this.getTnaMetaTags()); } this.gtag("set", { allow_ad_personalization_signals: false }); } From 4ae43d09640262a820a3c3914250374df085702f Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 8 Feb 2024 14:54:48 +0000 Subject: [PATCH 11/18] Improve getTnaMetaTags --- src/nationalarchives/analytics.mjs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 616a11bb..a211f997 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -121,23 +121,16 @@ class EventTracker { this.events.push({ event: eventName, data }); } + /** @protected */ getTnaMetaTags() { - // return Object.fromEntries( - // Array.from( - // document.head.querySelectorAll("meta[name^='tna:'][content]"), - // ).map(($metaEl) => [ - // metaData[$metaEl.getAttribute("name").replace(/^tna:/, "")], - // $metaEl.getAttribute("content"), - // ]), - // ); - const metaData = {}; - Array.from( - document.head.querySelectorAll("meta[name^='tna:'][content]"), - ).forEach(($metaEl) => { - metaData[$metaEl.getAttribute("name").replace(/^tna:/, "")] = - $metaEl.getAttribute("content"); - }); - return metaData; + return Object.fromEntries( + Array.from( + document.head.querySelectorAll("meta[name^='tna:'][content]"), + ).map(($metaEl) => [ + $metaEl.getAttribute("name").replace(/^tna:/, ""), + $metaEl.getAttribute("content"), + ]), + ); } } From d5c96b1672931079c5fba3a31931ae902c703a80 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Fri, 9 Feb 2024 10:56:20 +0000 Subject: [PATCH 12/18] Remove app-specific tracking, export helpers, change GA4 implementation --- src/nationalarchives/analytics.mjs | 105 ++++++++--------------------- 1 file changed, 28 insertions(+), 77 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index a211f997..4739c622 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -1,5 +1,9 @@ import Cookies from "./lib/cookies.mjs"; -import { getXPathTo, valueGetters } from "./lib/analytics-helpers.mjs"; +import { + getXPathTo, + getClosestHeading, + valueGetters, +} from "./lib/analytics-helpers.mjs"; import BreadcrumbAnalytics from "./components/breadcrumbs/analytics.js"; import HeaderAnalytics from "./components/header/analytics.js"; import HeroAnalytics from "./components/hero/analytics.js"; @@ -118,16 +122,16 @@ class EventTracker { /** @protected */ recordEvent(eventName, data) { - this.events.push({ event: eventName, data }); + this.events.push({ event: eventName, "tna.data": data }); } /** @protected */ getTnaMetaTags() { return Object.fromEntries( Array.from( - document.head.querySelectorAll("meta[name^='tna:'][content]"), + document.head.querySelectorAll("meta[name^='tna.'][content]"), ).map(($metaEl) => [ - $metaEl.getAttribute("name").replace(/^tna:/, ""), + $metaEl.getAttribute("name"), $metaEl.getAttribute("content"), ]), ); @@ -166,15 +170,17 @@ class GA4 extends EventTracker { /** @protected */ recordEvent(eventName, data) { - const ga4Data = { event: eventName, data }; - window.dataLayer.push(ga4Data); - // console.log(window.dataLayer); + const ga4Data = { event: eventName, "tna.data": data }; + this.pushToDataLayer(ga4Data); } /** @protected */ gtag() { - window.dataLayer.push(arguments); - // console.log(window.dataLayer); + this.pushToDataLayer(arguments); + } + + pushToDataLayer(data) { + window.dataLayer.push(data); } /** @protected */ @@ -183,18 +189,19 @@ class GA4 extends EventTracker { window["ga-disable-GA_MEASUREMENT_ID"] = false; this.trackingEnabled = true; if (!this.trackingCodeAdded) { + this.pushToDataLayer({ + "gtm.start": new Date().getTime(), + event: "gtm.js", + }); + const firstScript = document.getElementsByTagName("script")[0]; const script = document.createElement("script"); - script.setAttribute("async", true); - script.setAttribute( - "src", - `https://www.googletagmanager.com/gtm.js?id=${this.gTagId}&l=dataLayer`, - ); - // document.head.appendChild(script); - this.gtag("js", new Date()); + script.async = true; + script.src = `https://www.googletagmanager.com/gtm.js?id=${this.gTagId}&l=dataLayer`; + firstScript.parentNode.insertBefore(script, firstScript); this.trackingCodeAdded = true; - window.dataLayer.push(this.getTnaMetaTags()); + this.pushToDataLayer(this.getTnaMetaTags()); } - this.gtag("set", { allow_ad_personalization_signals: false }); + this.gtag("set", { allow_google_signals: true }); } } @@ -202,68 +209,12 @@ class GA4 extends EventTracker { disableTracking() { if (this.trackingEnabled) { window["ga-disable-GA_MEASUREMENT_ID"] = true; - this.gtag("set", { allow_ad_personalization_signals: true }); + this.gtag("set", { allow_google_signals: false }); this.trackingEnabled = false; } } } -/* - * ========================================== - * TEMP TESTING - * ========================================== - */ -const analytics = new GA4("test"); - -analytics.addListener(".etna-article__sidebar", "sidebar", [ - { - eventName: "scection_jump", - targetElement: ".etna-article__sidebar-item", - on: "click", - data: { - value: valueGetters.text, - }, - }, -]); - -analytics.addListener(".etna-article", "article", [ - { - eventName: "toggle_section", - targetElement: ".etna-article__section-button", - on: "click", - data: { - // eslint-disable-next-line no-unused-vars - state: ($el, $scope, event) => { - const expanded = $el.getAttribute("aria-expanded"); - if (expanded === null) { - return null; - } - return expanded.toString() === "true" ? "opened" : "closed"; - }, - value: valueGetters.text, - }, - }, -]); - -analytics.addListener(document.documentElement, "doc", [ - { - eventName: "double_click", - on: "dblclick", - data: { - state: ($el, $scope, event) => getXPathTo(event.target), - value: ($el, $scope, event) => event.target.innerHTML, - }, - }, -]); - -analytics.addListener(document.getElementById("tna-form__search"), "search", [ - { - eventName: "search_term_blur", - on: "blur", - data: { - value: valueGetters.value, - }, - }, -]); +const helpers = { getXPathTo, getClosestHeading, valueGetters }; -export { EventTracker, GA4 }; +export { EventTracker, GA4, helpers }; From d9b571899afe72cd47c5bdc0e249153a1c7a852f Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Tue, 13 Feb 2024 13:37:31 +0000 Subject: [PATCH 13/18] Change GA4 event path --- src/nationalarchives/analytics.mjs | 2 +- tasks/generate-fixture-html.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 4739c622..a1c69d2f 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -170,7 +170,7 @@ class GA4 extends EventTracker { /** @protected */ recordEvent(eventName, data) { - const ga4Data = { event: eventName, "tna.data": data }; + const ga4Data = { event: eventName, "tna.event": data }; this.pushToDataLayer(ga4Data); } diff --git a/tasks/generate-fixture-html.js b/tasks/generate-fixture-html.js index 5c07da5e..914600cd 100644 --- a/tasks/generate-fixture-html.js +++ b/tasks/generate-fixture-html.js @@ -12,10 +12,6 @@ if (!fs.existsSync(fixturesOutputDirectory)) { fs.mkdirSync(fixturesOutputDirectory); } -if (!fs.existsSync("temp")) { - fs.mkdirSync("temp"); -} - const components = globSync( `${componentsDirectory}*${componentFixturesFile}`, ).map((componentFixtureFile) => From 9fe0f3af6893433e7f95e206e4ddfb24c313784c Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Tue, 13 Feb 2024 13:41:18 +0000 Subject: [PATCH 14/18] Update GitHub actions --- .github/workflows/publish-storybook.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-storybook.yml b/.github/workflows/publish-storybook.yml index 3c88c3fb..611a7aaa 100644 --- a/.github/workflows/publish-storybook.yml +++ b/.github/workflows/publish-storybook.yml @@ -42,12 +42,12 @@ jobs: run: npm ci - name: Build Storybook run: npm run build - - uses: actions/configure-pages@v3 - - uses: actions/upload-pages-artifact@v1 + - uses: actions/configure-pages@v4 + - uses: actions/upload-pages-artifact@v3 with: path: storybook - id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 chromatic: runs-on: ubuntu-latest From 58a81f9f95661dc1513e1e66befba71102a0c79f Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Wed, 14 Feb 2024 14:36:17 +0000 Subject: [PATCH 15/18] Add tracking for global header --- src/nationalarchives/analytics.mjs | 2 ++ .../components/global-header/analytics.js | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/nationalarchives/components/global-header/analytics.js diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index a1c69d2f..7beede0a 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -5,12 +5,14 @@ import { valueGetters, } from "./lib/analytics-helpers.mjs"; import BreadcrumbAnalytics from "./components/breadcrumbs/analytics.js"; +import GlobalHeaderAnalytics from "./components/global-header/analytics.js"; import HeaderAnalytics from "./components/header/analytics.js"; import HeroAnalytics from "./components/hero/analytics.js"; import PictureAnalytics from "./components/picture/analytics.js"; const componentAnalytics = [ ...BreadcrumbAnalytics, + ...GlobalHeaderAnalytics, ...HeaderAnalytics, ...HeroAnalytics, ...PictureAnalytics, diff --git a/src/nationalarchives/components/global-header/analytics.js b/src/nationalarchives/components/global-header/analytics.js new file mode 100644 index 00000000..92a37a92 --- /dev/null +++ b/src/nationalarchives/components/global-header/analytics.js @@ -0,0 +1,23 @@ +export default [ + { + scope: ".tna-global-header", + areaName: "header", + events: [ + { + eventName: "toggle", + targetElement: ".tna-global-header__navigation-button", + on: "click", + data: { + // eslint-disable-next-line no-unused-vars + state: ($el, $scope, event) => { + const expanded = $el.getAttribute("aria-expanded"); + if (expanded === null) { + return null; + } + return expanded.toString() === "true" ? "opened" : "closed"; + }, + }, + }, + ], + }, +]; From 2600bf86aa9cc936a11a5db253656b9ff2b237b1 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Thu, 15 Feb 2024 11:32:07 +0000 Subject: [PATCH 16/18] Add more component analytics --- src/nationalarchives/analytics.mjs | 14 +++++++++++ .../components/checkboxes/analytics.js | 25 +++++++++++++++++++ .../components/radios/analytics.js | 25 +++++++++++++++++++ .../components/search-field/analytics.js | 22 ++++++++++++++++ .../components/text-input/analytics.js | 23 +++++++++++++++++ .../components/textarea/analytics.js | 23 +++++++++++++++++ .../lib/analytics-helpers.mjs | 2 ++ 7 files changed, 134 insertions(+) create mode 100644 src/nationalarchives/components/checkboxes/analytics.js create mode 100644 src/nationalarchives/components/radios/analytics.js create mode 100644 src/nationalarchives/components/search-field/analytics.js create mode 100644 src/nationalarchives/components/text-input/analytics.js create mode 100644 src/nationalarchives/components/textarea/analytics.js diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index 7beede0a..f5ecb83d 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -5,17 +5,27 @@ import { valueGetters, } from "./lib/analytics-helpers.mjs"; import BreadcrumbAnalytics from "./components/breadcrumbs/analytics.js"; +import CheckboxesAnalytics from "./components/checkboxes/analytics.js"; import GlobalHeaderAnalytics from "./components/global-header/analytics.js"; import HeaderAnalytics from "./components/header/analytics.js"; import HeroAnalytics from "./components/hero/analytics.js"; import PictureAnalytics from "./components/picture/analytics.js"; +import RadiosAnalytics from "./components/radios/analytics.js"; +import SearchFieldAnalytics from "./components/search-field/analytics.js"; +import TextInputAnalytics from "./components/text-input/analytics.js"; +import TextareaAnalytics from "./components/textarea/analytics.js"; const componentAnalytics = [ ...BreadcrumbAnalytics, + ...CheckboxesAnalytics, ...GlobalHeaderAnalytics, ...HeaderAnalytics, ...HeroAnalytics, ...PictureAnalytics, + ...RadiosAnalytics, + ...SearchFieldAnalytics, + ...TextInputAnalytics, + ...TextareaAnalytics, ]; class EventTracker { @@ -113,6 +123,10 @@ class EventTracker { typeof eventDataInit.state === "function" ? eventDataInit.state.call(this, $el, $scope, event) : eventDataInit.state || null, + group: + typeof eventDataInit.group === "function" + ? eventDataInit.group.call(this, $el, $scope, event) + : eventDataInit.group || null, scope: getXPathTo($scope), targetElement: targetElement, timestamp: new Date().toISOString(), diff --git a/src/nationalarchives/components/checkboxes/analytics.js b/src/nationalarchives/components/checkboxes/analytics.js new file mode 100644 index 00000000..407ab7cd --- /dev/null +++ b/src/nationalarchives/components/checkboxes/analytics.js @@ -0,0 +1,25 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-checkboxes", + areaName: "checkboxes", + events: [ + { + eventName: "toggle", + targetElement: ".tna-checkboxes__item input", + on: "change", + data: { + state: valueGetters.checked, + // eslint-disable-next-line no-unused-vars + value: ($el, $scope, event) => $el.parentNode.innerText, + // eslint-disable-next-line no-unused-vars + group: ($el, $scope, event) => + $scope + .closest(".tna-form__group") + ?.querySelector(".tna-form__heading")?.innerText, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/radios/analytics.js b/src/nationalarchives/components/radios/analytics.js new file mode 100644 index 00000000..a38ed33b --- /dev/null +++ b/src/nationalarchives/components/radios/analytics.js @@ -0,0 +1,25 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-radios", + areaName: "radios", + events: [ + { + eventName: "toggle", + targetElement: ".tna-radios__item input", + on: "change", + data: { + state: valueGetters.checked, + // eslint-disable-next-line no-unused-vars + value: ($el, $scope, event) => $el.parentNode.innerText, + // eslint-disable-next-line no-unused-vars + group: ($el, $scope, event) => + $scope + .closest(".tna-form__group") + ?.querySelector(".tna-form__heading")?.innerText, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/search-field/analytics.js b/src/nationalarchives/components/search-field/analytics.js new file mode 100644 index 00000000..b029aed3 --- /dev/null +++ b/src/nationalarchives/components/search-field/analytics.js @@ -0,0 +1,22 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-search-field", + areaName: "search-field", + events: [ + { + eventName: "blurred", + targetElement: ".tna-search-field__input", + on: "blur", + data: { + // eslint-disable-next-line no-unused-vars + value: valueGetters.value, + // eslint-disable-next-line no-unused-vars + group: ($el, $scope, event) => + $scope.querySelector(".tna-form__heading")?.innerText, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/text-input/analytics.js b/src/nationalarchives/components/text-input/analytics.js new file mode 100644 index 00000000..cb5b9087 --- /dev/null +++ b/src/nationalarchives/components/text-input/analytics.js @@ -0,0 +1,23 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-text-input", + areaName: "text-input", + events: [ + { + eventName: "blurred", + on: "blur", + data: { + // eslint-disable-next-line no-unused-vars + value: valueGetters.value, + // eslint-disable-next-line no-unused-vars + group: ($el, $scope, event) => + $scope + .closest(".tna-form__group") + ?.querySelector(".tna-form__heading")?.innerText, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/components/textarea/analytics.js b/src/nationalarchives/components/textarea/analytics.js new file mode 100644 index 00000000..a6f6e49c --- /dev/null +++ b/src/nationalarchives/components/textarea/analytics.js @@ -0,0 +1,23 @@ +import { valueGetters } from "../../lib/analytics-helpers.mjs"; + +export default [ + { + scope: ".tna-textarea", + areaName: "textarea", + events: [ + { + eventName: "blurred", + on: "blur", + data: { + // eslint-disable-next-line no-unused-vars + value: valueGetters.value, + // eslint-disable-next-line no-unused-vars + group: ($el, $scope, event) => + $scope + .closest(".tna-form__group") + ?.querySelector(".tna-form__heading")?.innerText, + }, + }, + ], + }, +]; diff --git a/src/nationalarchives/lib/analytics-helpers.mjs b/src/nationalarchives/lib/analytics-helpers.mjs index cb839839..c341d306 100644 --- a/src/nationalarchives/lib/analytics-helpers.mjs +++ b/src/nationalarchives/lib/analytics-helpers.mjs @@ -56,6 +56,8 @@ const valueGetters = { html: ($el, $scope, event) => $el.innerHTML, // eslint-disable-next-line no-unused-vars value: ($el, $scope, event) => $el.value, + // eslint-disable-next-line no-unused-vars + checked: ($el, $scope, event) => ($el.checked ? "checked" : "unchecked"), }; export { getXPathTo, getClosestHeading, valueGetters }; From f93b04f48446dabb063064a1d9cae0bd7c6e7f1e Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Fri, 1 Mar 2024 15:05:36 +0000 Subject: [PATCH 17/18] Add more JSDocs --- src/nationalarchives/analytics.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/nationalarchives/analytics.mjs b/src/nationalarchives/analytics.mjs index f5ecb83d..6ce054ce 100644 --- a/src/nationalarchives/analytics.mjs +++ b/src/nationalarchives/analytics.mjs @@ -96,6 +96,7 @@ class EventTracker { }); } + /** @protected */ generateEventName(areaName, componentTracking) { return `${areaName}.${componentTracking.eventName || componentTracking.on}`; } @@ -195,6 +196,7 @@ class GA4 extends EventTracker { this.pushToDataLayer(arguments); } + /** @protected */ pushToDataLayer(data) { window.dataLayer.push(data); } From 4dbce44a808fa4c6c4af15ff9dfc129ccfd9a521 Mon Sep 17 00:00:00 2001 From: Andrew Hosgood Date: Fri, 1 Mar 2024 18:03:41 +0000 Subject: [PATCH 18/18] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8085461a..0c2fb8d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Error summary component +- Initial release of analytics library ### Changed ### Deprecated