Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Header and footer analytics #100

Merged
merged 14 commits into from
Mar 8, 2024
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- The default pagination style is plain buttons but can be changed with the `solid` option
- Updated the HTML of the headers, adding some attributes to the HTML to reduce reliance on JavaScript
- Switched from using `<ul>` to `<menu>` in the headers and footer
- Added some attributes to the HTML of the headers to reduce reliance on the JavaScript
- Cards without images now get a top border

Expand Down
153 changes: 98 additions & 55 deletions src/nationalarchives/analytics.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from "./lib/analytics-helpers.mjs";
import BreadcrumbAnalytics from "./components/breadcrumbs/analytics.js";
import CheckboxesAnalytics from "./components/checkboxes/analytics.js";
import FooterAnalytics from "./components/footer/analytics.js";
import GlobalHeaderAnalytics from "./components/global-header/analytics.js";
import HeaderAnalytics from "./components/header/analytics.js";
import HeroAnalytics from "./components/hero/analytics.js";
Expand All @@ -18,6 +19,7 @@ import TextareaAnalytics from "./components/textarea/analytics.js";
const componentAnalytics = [
...BreadcrumbAnalytics,
...CheckboxesAnalytics,
...FooterAnalytics,
...GlobalHeaderAnalytics,
...HeaderAnalytics,
...HeroAnalytics,
Expand All @@ -38,7 +40,19 @@ class EventTracker {
/** @protected */
start = new Date();

constructor() {
/** @protected */
prefix = "tna";

constructor(prefix) {
if (prefix) {
this.prefix = prefix;
}
}

/**
* Initialise all TNA Frontend component analytics.
*/
initAll() {
componentAnalytics.forEach((componentConfig) => {
this.addListener(
componentConfig.scope,
Expand All @@ -65,88 +79,97 @@ class EventTracker {
return;
}
scopeArray.forEach(($scope) => {
events.forEach((componentTracking) => {
if (!componentTracking.on) {
events.forEach((eventConfig) => {
if (!eventConfig.on) {
return;
}
if (componentTracking.targetElement) {
if (eventConfig.targetElement) {
Array.from(
$scope.querySelectorAll(componentTracking.targetElement),
).forEach(($el) =>
$scope.querySelectorAll(eventConfig.targetElement),
).forEach(($el, index) =>
this.attachListener(
$el,
componentTracking.on,
$scope,
this.generateEventName(areaName, componentTracking),
componentTracking.data,
componentTracking.targetElement,
this.generateEventName(areaName, eventConfig),
eventConfig,
scope,
areaName,
index + 1,
),
);
} else {
this.attachListener(
$scope,
componentTracking.on,
$scope,
this.generateEventName(areaName, componentTracking),
componentTracking.data,
componentTracking.targetElement,
this.generateEventName(areaName, eventConfig),
eventConfig,
scope,
areaName,
1,
);
}
});
});
}

/** @protected */
generateEventName(areaName, componentTracking) {
return `${areaName}.${componentTracking.eventName || componentTracking.on}`;
generateEventName(areaName, eventConfig) {
return `${this.prefix}.${areaName}.${eventConfig.eventName || eventConfig.on}`;
}

/** @protected */
attachListener(
$el,
eventTrigger,
$scope,
eventName,
eventDataInit,
targetElement,
) {
if (!$el) {
return;
}
$el.addEventListener(eventTrigger, (event) =>
this.recordEvent(eventName, {
...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,
group:
typeof eventDataInit.group === "function"
? eventDataInit.group.call(this, $el, $scope, event)
: eventDataInit.group || null,
scope: getXPathTo($scope),
targetElement: targetElement,
timestamp: new Date().toISOString(),
uri: window.location.pathname,
timeSincePageLoad: new Date() - this.start,
}),
attachListener($el, $scope, eventName, eventConfig, scope, areaName, index) {
const { on, data, targetElement, rootData = {} } = eventConfig;
$el.addEventListener(on, (event) =>
this.recordEvent(
eventName,
{
...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),
targetElement: targetElement,
// timestamp: new Date().toISOString(),
// uri: window.location.pathname,
timeSincePageLoad: new Date() - this.start,
index,
scope,
areaName,
},
Object.fromEntries(
Object.entries(rootData).map(([key, value]) => [
key,
this.computedValue(value, $el, $scope, event, index),
]),
),
),
);
}

/** @protected */
recordEvent(eventName, data) {
this.events.push({ event: eventName, "tna.data": data });
computedValue(value, $el, $scope, event, index) {
return typeof value === "function"
? value.call(this, $el, $scope, event, index)
: value || null;
}

/** @protected */
recordEvent(eventName, data, rootData = {}) {
this.events.push({
event: eventName,
[`${this.prefix}.event`]: data,
...rootData,
});
}

/** @protected */
getTnaMetaTags() {
return Object.fromEntries(
Array.from(
document.head.querySelectorAll("meta[name^='tna.'][content]"),
document.head.querySelectorAll(
`meta[name^='${this.prefix}.'][content]`,
),
).map(($metaEl) => [
$metaEl.getAttribute("name"),
$metaEl.getAttribute("content"),
Expand All @@ -167,8 +190,16 @@ class GA4 extends EventTracker {
trackingEnabled = false;
gTagId;

constructor(id) {
super();
constructor(id = null, options = {}) {
if (GA4._instance) {
return GA4._instance;
}
if (!id) {
throw Error("ID was not specified");
}
const { prefix = null, initAll = true } = options;
super(prefix);
GA4._instance = this;
this.gTagId = id;
window.dataLayer = window.dataLayer || [];
if (this.cookies.isPolicyAccepted("usage")) {
Expand All @@ -183,11 +214,23 @@ class GA4 extends EventTracker {
}
}
});
if (initAll) {
this.initAll();
}
}

/** @protected */
recordEvent(eventName, data) {
const ga4Data = { event: eventName, "tna.event": data };
recordEvent(eventName, data, rootData = {}) {
const ga4Data = {
event: eventName,
...Object.fromEntries(
Object.entries(data).map(([key, value]) => [
`${this.prefix}.event.${key}`,
value,
]),
),
...rootData,
};
this.pushToDataLayer(ga4Data);
}

Expand Down
3 changes: 2 additions & 1 deletion src/nationalarchives/components/breadcrumbs/template.njk
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
{%- if params.noCollapse -%}
{%- set containerClasses = containerClasses.concat('tna-breadcrumbs--no-collapse') -%}
{%- endif -%}
<nav class="tna-breadcrumbs{{ ' ' + ( containerClasses | join(' ') ) if containerClasses.length else '' }}" data-module="tna-breadcrumbs" aria-label="Breadcrumb"{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
{%- set classes = containerClasses | join(' ') -%}
<nav class="tna-breadcrumbs{% if classes %} {{ classes }}{% endif %}" data-module="tna-breadcrumbs" aria-label="Breadcrumb"{%- for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
<ol class="tna-breadcrumbs__list" {%- if params.structuredData %} itemscope itemtype="https://schema.org/BreadcrumbList"{% endif %}>
{%- for item in params.items -%}
<li class="tna-breadcrumbs__item" {%- if params.structuredData %} itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem"{% endif %}>
Expand Down
6 changes: 3 additions & 3 deletions src/nationalarchives/components/button/fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"text": "Log in",
"href": "#"
},
"html": "<a href=\"#\" class=\"tna-button \">Log in</a>"
"html": "<a href=\"#\" class=\"tna-button\">Log in</a>"
},
{
"name": "with title",
Expand All @@ -16,7 +16,7 @@
"href": "#",
"title": "Log in to the service"
},
"html": "<a href=\"#\" class=\"tna-button \" title=\"Log in to the service\">Log in</a>"
"html": "<a href=\"#\" class=\"tna-button\" title=\"Log in to the service\">Log in</a>"
},
{
"name": "accent",
Expand Down Expand Up @@ -45,7 +45,7 @@
"data-testattribute": "foobar"
}
},
"html": "<a href=\"#\" class=\"tna-button \" data-testattribute=\"foobar\">Log in</a>"
"html": "<a href=\"#\" class=\"tna-button\" data-testattribute=\"foobar\">Log in</a>"
}
]
}
17 changes: 9 additions & 8 deletions src/nationalarchives/components/button/template.njk
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
{%- set buttonClasses = [params.classes] if params.classes else [] -%}
{%- set containerClasses = [params.classes] if params.classes else [] -%}
{%- if params.accent -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--accent') -%}
{%- set containerClasses = containerClasses.concat('tna-button--accent') -%}
{%- endif -%}
{%- if params.small -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--small') -%}
{%- set containerClasses = containerClasses.concat('tna-button--small') -%}
{%- endif -%}
{%- if params.plain -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--plain') -%}
{%- set containerClasses = containerClasses.concat('tna-button--plain') -%}
{%- endif -%}
{%- if params.iconOnly -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--icon-only') -%}
{%- set containerClasses = containerClasses.concat('tna-button--icon-only') -%}
{%- endif -%}
{%- if params.iconOnlyOnMobile -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--icon-only-mobile') -%}
{%- set containerClasses = containerClasses.concat('tna-button--icon-only-mobile') -%}
{%- endif -%}
{%- if params.rightAlignIcon -%}
{%- set buttonClasses = buttonClasses.concat('tna-button--icon-right') -%}
{%- set containerClasses = containerClasses.concat('tna-button--icon-right') -%}
{%- endif -%}
<{{ 'button' if params.buttonElement else 'a' }}{%- if not params.buttonElement %} href="{{ params.href }}"{%- endif %} class="tna-button {{ buttonClasses | join(' ') }}"{%- if params.buttonElement %} type="{{ params.buttonType or 'button' }}"{% endif -%}{%- if params.title %} title="{{ params.title }}"{% endif %}{% for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
{%- set classes = containerClasses | join(' ') -%}
<{{ 'button' if params.buttonElement else 'a' }}{%- if not params.buttonElement %} href="{{ params.href }}"{%- endif %} class="tna-button{% if classes %} {{ classes }}{% endif %}"{%- if params.buttonElement %} type="{{ params.buttonType or 'button' }}"{% endif -%}{%- if params.title %} title="{{ params.title }}"{% endif %}{% for attribute, value in params.attributes %} {{ attribute }}="{{ value }}"{% endfor %}>
{% if params.icon -%}
<i class="fa-solid fa-{{ params.icon }}" aria-hidden="true"></i>
{% endif -%}
Expand Down
Loading