From d294fc6eda53ecbdc772f8d8f26e4f5fe71a39bd Mon Sep 17 00:00:00 2001 From: Thomas Iles Date: Mon, 30 Mar 2026 14:41:45 +0100 Subject: [PATCH 1/3] Add new optional link tracker Only external links are tracked through google analytics. This commit adds a new javascript function to track clicks on links with the data-track-link attribute. Adding this attribute to a link, either external or internal, will cause the link to be tracked by google analytics. For example: Link text It's also possible to add it to mailto or tel links. The event has a set schema so would show in the GA dashboard as a navigation event and as a 'generic link'. That might not be what you want. --- app/frontend/entrypoints/application.js | 10 +- app/frontend/javascript/google-tag/index.js | 23 +++- .../javascript/google-tag/index.test.js | 117 +++++++++++++++++- 3 files changed, 143 insertions(+), 7 deletions(-) diff --git a/app/frontend/entrypoints/application.js b/app/frontend/entrypoints/application.js index 8f77944903..915b2ed8d2 100644 --- a/app/frontend/entrypoints/application.js +++ b/app/frontend/entrypoints/application.js @@ -7,14 +7,15 @@ import { installAnalyticsScript, sendPageViewEvent, attachExternalLinkTracker, - attachQuestionXsRoutesTracker + attachQuestionXsRoutesTracker, + attachOptionalLinkTracker } from '../javascript/google-tag' import { saveConsentStatus } from '../javascript/utils/cookie-consent' import ajaxMarkdownPreview from '../javascript/ajax-markdown-preview' document .querySelectorAll('[data-module="copy-to-clipboard"]') - .forEach(element => { + .forEach((element) => { copyToClipboard( element, element.querySelector('[data-copy-target]'), @@ -24,7 +25,7 @@ document document .querySelectorAll('[data-module="markdown-editor-toolbar"]') - .forEach(element => { + .forEach((element) => { markdownEditorToolbar( element, JSON.parse(element.getAttribute('data-i18n')), @@ -35,7 +36,7 @@ document document .querySelectorAll('[data-module="ajax-markdown-preview"]') - .forEach(element => { + .forEach((element) => { ajaxMarkdownPreview( element.querySelector('[data-ajax-markdown-target]'), element.querySelector('[data-ajax-markdown-source]'), @@ -50,6 +51,7 @@ if (document.body.dataset.googleAnalyticsEnabled === 'true') { sendPageViewEvent() attachExternalLinkTracker() attachQuestionXsRoutesTracker() + attachOptionalLinkTracker() } initAll() diff --git a/app/frontend/javascript/google-tag/index.js b/app/frontend/javascript/google-tag/index.js index e74b8afee1..46b275324d 100644 --- a/app/frontend/javascript/google-tag/index.js +++ b/app/frontend/javascript/google-tag/index.js @@ -1,7 +1,7 @@ export function installAnalyticsScript (global) { const GTAG_ID = 'GTM-MFJWJNW' if (!window.ga) { - ;(function (w, d, s, l, i) { + (function (w, d, s, l, i) { w[l] = w[l] || [] w[l].push({ 'gtm.start': new Date().getTime(), @@ -68,6 +68,27 @@ export function attachExternalLinkTracker () { }) } +export function attachOptionalLinkTracker () { + const linksToTrack = document.querySelectorAll('a[data-track-link]') + linksToTrack.forEach(function (link) { + link.addEventListener('click', function (event) { + const target = event.target + const external = target.getAttribute('href').startsWith('http') + window.dataLayer.push({ + event: 'event_data', + event_data: { + event_name: 'navigation', + external, + method: 'primary click', + text: target.textContent, + type: 'generic link', + url: target.href + } + }) + }) + }) +} + export function attachQuestionXsRoutesTracker () { const showRoutesPathRegex = /^\/forms\/\d+\/pages\/\d+\/routes$/ const path = window.location.pathname diff --git a/app/frontend/javascript/google-tag/index.test.js b/app/frontend/javascript/google-tag/index.test.js index b86947012e..97f3749714 100644 --- a/app/frontend/javascript/google-tag/index.test.js +++ b/app/frontend/javascript/google-tag/index.test.js @@ -7,7 +7,8 @@ import { sendPageViewEvent, attachExternalLinkTracker, setDefaultConsent, - attachQuestionXsRoutesTracker + attachQuestionXsRoutesTracker, + attachOptionalLinkTracker } from '../google-tag' import { describe, afterEach, it, expect, beforeEach } from 'vitest' @@ -151,7 +152,7 @@ describe('google_tag.mjs', () => { data: 'Some existing data in the dataLayer' } - const preventDefault = event => { + const preventDefault = (event) => { event.preventDefault() } @@ -243,4 +244,116 @@ describe('google_tag.mjs', () => { }) }) }) + + describe('attachOptionalLinkTracker', () => { + const preventDefault = (event) => { + event.preventDefault() + } + beforeEach(() => { + document.body.innerHTML = ` + A link to example.com + A link to example.com + A secure link to example.com + Dpwnload a CSV file + A link to example@example.com + ` + window.dataLayer = [] + + // stop link clicks from navigating, since jsdom can't do navigation + document.querySelector('a').addEventListener('click', preventDefault) + }) + + afterEach(() => { + document.querySelector('a').removeEventListener('click', preventDefault) + window.dataLayer = [] + }) + + it('tracks clicks on external HTTP link with data-track-link attribute', () => { + attachOptionalLinkTracker() + + const externalLink = document.getElementById('externalHTTP') + externalLink.click() + expect(window.dataLayer).toEqual([ + { + event: 'event_data', + event_data: { + event_name: 'navigation', + external: true, + method: 'primary click', + text: 'A link to example.com', + type: 'generic link', + url: 'http://example.com/' + } + } + ]) + }) + + it('tracks clicks on external HTTPS link with data-track-link attribute', () => { + attachOptionalLinkTracker() + + const externalLink = document.getElementById('externalHTTPS') + externalLink.click() + expect(window.dataLayer).toEqual([ + { + event: 'event_data', + event_data: { + event_name: 'navigation', + external: true, + method: 'primary click', + text: 'A secure link to example.com', + type: 'generic link', + url: 'https://example.com/' + } + } + ]) + }) + + it('tracks clicks on internal link with data-track-link attribute', () => { + attachOptionalLinkTracker() + + const internalLink = document.getElementById('internal') + internalLink.click() + expect(window.dataLayer).toEqual([ + { + event: 'event_data', + event_data: { + event_name: 'navigation', + external: false, + method: 'primary click', + text: 'Dpwnload a CSV file', + type: 'generic link', + url: 'http://localhost:3000/a_csv_file.csv' + } + } + ]) + }) + + it('does not track clicks on links without data-track-link attribute', () => { + attachOptionalLinkTracker() + + const link = document.getElementById('noTrack') + link.click() + expect(window.dataLayer).toEqual([]) + }) + + it('tracks clicks on mailto link with data-track-link attribute', () => { + attachOptionalLinkTracker() + + const link = document.getElementById('mailto') + link.click() + expect(window.dataLayer).toEqual([ + { + event: 'event_data', + event_data: { + event_name: 'navigation', + external: false, + method: 'primary click', + text: 'A link to example@example.com', + type: 'generic link', + url: 'mailto:example@example.com' + } + } + ]) + }) + }) }) From 66ed7c737d4ed689a021fcef3748421ad5141be4 Mon Sep 17 00:00:00 2001 From: Thomas Iles Date: Mon, 30 Mar 2026 14:42:12 +0100 Subject: [PATCH 2/3] Add track-link to Welsh translation CSV download We want to track clicks on the Welsh download CSV link, to see if people are using it. Add a track-link to the Welsh translation CSV download link. --- app/views/forms/welsh_translation/new.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/forms/welsh_translation/new.html.erb b/app/views/forms/welsh_translation/new.html.erb index 35d2507f6f..bde766a74c 100644 --- a/app/views/forms/welsh_translation/new.html.erb +++ b/app/views/forms/welsh_translation/new.html.erb @@ -14,7 +14,7 @@
- <%= govuk_button_link_to t(".csv_download"), welsh_translation_download_path(@welsh_translation_input.form), secondary: true %> + <%= govuk_button_link_to t(".csv_download"), welsh_translation_download_path(@welsh_translation_input.form), secondary: true, data: { "track-link": true } %> <%= render PreviewLinkComponent::View.new(@welsh_translation_input.form.pages, preview_link(@welsh_translation_input.form, locale: :cy), t(".preview_link_text")) %>
From 6b9042ae539864fb87e16719036d5bd5767e824e Mon Sep 17 00:00:00 2001 From: Thomas Iles Date: Mon, 30 Mar 2026 14:48:50 +0100 Subject: [PATCH 3/3] Add .csv to welsh translation download link We modify the welsh translation download link to include the .csv extension. This might make it easier to track the link in Google Analytics. --- app/views/forms/welsh_translation/new.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/forms/welsh_translation/new.html.erb b/app/views/forms/welsh_translation/new.html.erb index bde766a74c..c4e72dfd1a 100644 --- a/app/views/forms/welsh_translation/new.html.erb +++ b/app/views/forms/welsh_translation/new.html.erb @@ -14,7 +14,7 @@
- <%= govuk_button_link_to t(".csv_download"), welsh_translation_download_path(@welsh_translation_input.form), secondary: true, data: { "track-link": true } %> + <%= govuk_button_link_to t(".csv_download"), welsh_translation_download_path(@welsh_translation_input.form, format: :csv), secondary: true, data: { "track-link": true } %> <%= render PreviewLinkComponent::View.new(@welsh_translation_input.form.pages, preview_link(@welsh_translation_input.form, locale: :cy), t(".preview_link_text")) %>