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

Analytics #95

Merged
merged 29 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
249b9ec
Initial idea for analytics package
ahosgood Jan 24, 2024
a9f1e3b
Add more analytics functionality
ahosgood Jan 25, 2024
6289115
Define per-component analytics
ahosgood Jan 25, 2024
fe0cb79
Add GA4 handling
ahosgood Jan 25, 2024
7359b15
More resiliance for missing elements, time since load, test skeleton
ahosgood Jan 25, 2024
1cb2763
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Jan 26, 2024
18b0395
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Jan 26, 2024
4c2d448
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 5, 2024
dbf132f
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 7, 2024
dd40514
Change HTML fixtures test location
ahosgood Feb 7, 2024
0e063c0
Add more information to picture caption clicks
ahosgood Feb 7, 2024
9d93286
Abstract getClosestHeading
ahosgood Feb 7, 2024
1df1880
Remove trailing slash on some <br> elements
ahosgood Feb 8, 2024
e514e1e
Get meta tags from HTML
ahosgood Feb 8, 2024
4ae43d0
Improve getTnaMetaTags
ahosgood Feb 8, 2024
4d6834b
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 9, 2024
d5c96b1
Remove app-specific tracking, export helpers, change GA4 implementation
ahosgood Feb 9, 2024
b215b3c
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 13, 2024
d9b5718
Change GA4 event path
ahosgood Feb 13, 2024
9fe0f3a
Update GitHub actions
ahosgood Feb 13, 2024
ca022fc
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 14, 2024
58a81f9
Add tracking for global header
ahosgood Feb 14, 2024
95a7b6a
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 15, 2024
2600bf8
Add more component analytics
ahosgood Feb 15, 2024
dc5feee
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Feb 20, 2024
85d1834
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Mar 1, 2024
f93b04f
Add more JSDocs
ahosgood Mar 1, 2024
4dbce44
Update CHANGELOG.md
ahosgood Mar 1, 2024
5e586d2
Merge branch 'main' of github.com:nationalarchives/tna-frontend into …
ahosgood Mar 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/publish-storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ build-storybook.log
chromatic.config.json
chromatic.log
RELEASE_NOTES.txt
temp
fixtures-html
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
- Initial idea for search filters
- Allow structured data in breadcrumbs with `structuredData`

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
238 changes: 238 additions & 0 deletions src/nationalarchives/analytics.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
import Cookies from "./lib/cookies.mjs";
import {
getXPathTo,
getClosestHeading,
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 {
/** @protected */
cookies = new (window.TNAFrontend?.Cookies || Cookies)();

/** @protected */
events = [];

/** @protected */
start = new Date();

constructor() {
componentAnalytics.forEach((componentConfig) => {
this.addListener(
componentConfig.scope,
componentConfig.areaName,
componentConfig.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, 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;
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) => {
if (!componentTracking.on) {
return;
}
if (componentTracking.targetElement) {
Array.from(
$scope.querySelectorAll(componentTracking.targetElement),
).forEach(($el) =>
this.attachListener(
$el,
componentTracking.on,
$scope,
this.generateEventName(areaName, componentTracking),
componentTracking.data,
componentTracking.targetElement,
),
);
} else {
this.attachListener(
$scope,
componentTracking.on,
$scope,
this.generateEventName(areaName, componentTracking),
componentTracking.data,
componentTracking.targetElement,
);
}
});
});
}

/** @protected */
generateEventName(areaName, componentTracking) {
return `${areaName}.${componentTracking.eventName || componentTracking.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,
}),
);
}

/** @protected */
recordEvent(eventName, data) {
this.events.push({ event: eventName, "tna.data": data });
}

/** @protected */
getTnaMetaTags() {
return Object.fromEntries(
Array.from(
document.head.querySelectorAll("meta[name^='tna.'][content]"),
).map(($metaEl) => [
$metaEl.getAttribute("name"),
$metaEl.getAttribute("content"),
]),
);
}
}

/**
* 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();
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, "tna.event": data };
this.pushToDataLayer(ga4Data);
}

/** @protected */
gtag() {
this.pushToDataLayer(arguments);
}

/** @protected */
pushToDataLayer(data) {
window.dataLayer.push(data);
}

/** @protected */
enableTracking() {
if (!this.trackingEnabled) {
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.async = true;
script.src = `https://www.googletagmanager.com/gtm.js?id=${this.gTagId}&l=dataLayer`;
firstScript.parentNode.insertBefore(script, firstScript);
this.trackingCodeAdded = true;
this.pushToDataLayer(this.getTnaMetaTags());
}
this.gtag("set", { allow_google_signals: true });
}
}

/** @protected */
disableTracking() {
if (this.trackingEnabled) {
window["ga-disable-GA_MEASUREMENT_ID"] = true;
this.gtag("set", { allow_google_signals: false });
this.trackingEnabled = false;
}
}
}

const helpers = { getXPathTo, getClosestHeading, valueGetters };

export { EventTracker, GA4, helpers };
17 changes: 17 additions & 0 deletions src/nationalarchives/components/breadcrumbs/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { valueGetters } from "../../lib/analytics-helpers.mjs";

export default [
{
scope: ".tna-breadcrumbs",
areaName: "breadcrumbs",
events: [
{
eventName: "click",
targetElement:
".tna-breadcrumbs__item--expandable button.tna-breadcrumbs__link",
on: "click",
data: { state: "expand", value: valueGetters.html },
},
],
},
];
25 changes: 25 additions & 0 deletions src/nationalarchives/components/checkboxes/analytics.js
Original file line number Diff line number Diff line change
@@ -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,
},
},
],
},
];
23 changes: 23 additions & 0 deletions src/nationalarchives/components/global-header/analytics.js
Original file line number Diff line number Diff line change
@@ -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";
},
},
},
],
},
];
23 changes: 23 additions & 0 deletions src/nationalarchives/components/header/analytics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default [
{
scope: ".tna-header",
areaName: "header",
events: [
{
eventName: "toggle",
targetElement: ".tna-header__navigation-toggle-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";
},
},
},
],
},
];
Loading