From eccaecce2414037a9901221bf50b339c7bb6856b Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Fri, 19 Jun 2026 09:30:10 +0100 Subject: [PATCH 1/6] Remove usage data tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current implementation uses Universal Analytics, which was retired by Google in 2024 and replaced with GA4. I don't believe the usage data is actually being collected, nor that we have any way to view it. If we think that tracking usage data is valuable, we'll need to reimplement the functionality using GA4, or an alternative better suited to tracking app usage. This will also allow us to remove the dependency on universal-analytics, which removes a dependency on a vulnerable version of uuid – see #2543. --- lib/dev-server.js | 2 - lib/usage-data.js | 120 ----------------------------------------- lib/usage-data.test.js | 77 -------------------------- 3 files changed, 199 deletions(-) delete mode 100644 lib/usage-data.js delete mode 100644 lib/usage-data.test.js diff --git a/lib/dev-server.js b/lib/dev-server.js index a5ae807afa..d967e9305b 100644 --- a/lib/dev-server.js +++ b/lib/dev-server.js @@ -16,7 +16,6 @@ const { setNodemonInstance } = require('./build') const plugins = require('./plugins/plugins') -const { collectDataUsage } = require('./usage-data') const utils = require('./utils') const { packageDir, @@ -32,7 +31,6 @@ const { logPerformanceSummaryOnce, startPerformanceTimer, endPerformanceTimer } // Build watch and serve async function runDevServer () { - await collectDataUsage() let startupError try { diff --git a/lib/usage-data.js b/lib/usage-data.js deleted file mode 100644 index 536a5874f0..0000000000 --- a/lib/usage-data.js +++ /dev/null @@ -1,120 +0,0 @@ -// core dependencies -const fs = require('fs') -const os = require('os') -const path = require('path') -const { randomUUID } = require('node:crypto') - -// npm dependencies -const { default: confirm } = require('@inquirer/confirm') -const universalAnalytics = require('universal-analytics') -const ansiColors = require('ansi-colors') - -// local dependencies -const packageJson = require('../package.json') -const { projectDir } = require('./utils/paths') - -const usageDataFilePath = path.join(projectDir, 'usage-data-config.json') -const headline = ansiColors.underline('Help us improve the GOV.UK Prototype Kit') - -const USAGE_DATA_PROMPT = ` -${headline} - -With your permission, the kit can send useful anonymous usage data -for analysis to help the team improve the service. Read more here: -https://prototype-kit.service.gov.uk/docs/usage-data - -Do you give permission for the kit to send anonymous usage data? -`.trim() - -function getUsageDataConfig () { - // Try to read config file to see if usage data is opted in - let usageDataConfig = {} - try { - usageDataConfig = require(usageDataFilePath) - } catch (e) { - // do nothing - we will make a config - } - return usageDataConfig -} - -function setUsageDataConfig (usageDataConfig) { - const usageDataConfigJSON = JSON.stringify(usageDataConfig, null, ' ') - try { - fs.writeFileSync(usageDataFilePath, usageDataConfigJSON) - return true - } catch (error) { - console.error(error) - } - return false -} - -// Ask for permission to track data -// returns a Promise with the user's answer -async function askForUsageDataPermission () { - if (process.stdout.isTTY) { - return confirm({ - message: USAGE_DATA_PROMPT, - default: false - }) - } - - return false -} - -function startTracking (usageDataConfig) { - // Get client ID for tracking - // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid - - if (usageDataConfig.clientId === undefined) { - usageDataConfig.clientId = randomUUID() - module.exports.setUsageDataConfig(usageDataConfig) - } - - // Track kit start event, with kit version, operating system and Node.js version - const trackingId = 'UA-26179049-21' - const trackingUser = universalAnalytics(trackingId, usageDataConfig.clientId) - - const kitVersion = packageJson.version - const operatingSystem = os.platform() + ' ' + os.release() - const nodeVersion = process.versions.node - - // Anonymise the IP - trackingUser.set('anonymizeIp', 1) - - // Set custom dimensions - trackingUser.set('cd1', operatingSystem) - trackingUser.set('cd2', kitVersion) - trackingUser.set('cd3', nodeVersion) - - // Trigger start event - trackingUser.event('State', 'Start').send() -} - -async function collectDataUsage () { - // Get usageDataConfig from file, if exists - const usageDataConfig = module.exports.getUsageDataConfig() - - if (usageDataConfig.collectUsageData === undefined) { - // No recorded answer, so ask for permission - const permissionGranted = await module.exports.askForUsageDataPermission() - usageDataConfig.collectUsageData = permissionGranted - module.exports.setUsageDataConfig(usageDataConfig) - - if (permissionGranted) { - module.exports.startTracking(usageDataConfig) - } - } else { - if (usageDataConfig.collectUsageData === true) { - // Opted in - module.exports.startTracking(usageDataConfig) - } - } -} - -module.exports = { - getUsageDataConfig, - setUsageDataConfig, - askForUsageDataPermission, - startTracking, - collectDataUsage -} diff --git a/lib/usage-data.test.js b/lib/usage-data.test.js deleted file mode 100644 index 65cd7efd5f..0000000000 --- a/lib/usage-data.test.js +++ /dev/null @@ -1,77 +0,0 @@ -const { askForUsageDataPermission } = require('./usage-data') - -jest.mock('@inquirer/confirm') - -const { default: confirm } = require('@inquirer/confirm') - -describe('askForUsageDataPermission', () => { - afterEach(() => { - jest.resetAllMocks() - }) - - describe('when in an interactive shell', () => { - // We can't use jest.replaceProperty here because in a non-TTY environment - // the isTTY property doesn't exist on process.stdout, and Jest will only - // replace existing properties. - let originalTTY - - beforeAll(() => { - originalTTY = process.stdout.isTTY - process.stdout.isTTY = true - }) - - afterAll(() => { - process.stdout.isTTY = originalTTY - }) - - it('asks the user for confirmation', () => { - askForUsageDataPermission() - - expect(confirm).toHaveBeenCalledTimes(1) - expect(confirm).toHaveBeenCalledWith({ - default: false, - message: expect.stringContaining( - 'Do you give permission for the kit to send anonymous usage data?' - ) - }) - }) - - it('returns true if the user accepts tracking', async () => { - confirm.mockResolvedValue(true) - - expect(await askForUsageDataPermission()).toBe(true) - }) - - it('returns false if the user does not accept tracking', async () => { - confirm.mockResolvedValue(false) - - expect(await askForUsageDataPermission()).toBe(false) - }) - }) - - describe('when in a non-interactive shell', () => { - // We can't use jest.replaceProperty here because in a non-TTY environment - // the isTTY property doesn't exist on process.stdout, and Jest will only - // replace existing properties. - let originalTTY - - beforeAll(() => { - originalTTY = process.stdout.isTTY - process.stdout.isTTY = undefined - }) - - afterAll(() => { - process.stdout.isTTY = originalTTY - }) - - it('does not ask the user for confirmation', () => { - askForUsageDataPermission() - - expect(confirm).not.toHaveBeenCalled() - }) - - it('returns false', async () => { - expect(await askForUsageDataPermission()).toBe(false) - }) - }) -}) From 7ea567bb4f228ce6a997ad9b44d33b50b7e2133f Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Fri, 19 Jun 2026 09:31:01 +0100 Subject: [PATCH 2/6] Remove dependency universal-analytics --- npm-shrinkwrap.json | 65 +++------------------------------------------ package.json | 3 +-- 2 files changed, 5 insertions(+), 63 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 05cb3c27e1..8d9297b6c2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -29,8 +29,7 @@ "portscanner": "^2.2.0", "sass": "^1.89.2", "semver": "^7.7.2", - "tar-stream": "^3.1.7", - "universal-analytics": "^0.5.3" + "tar-stream": "^3.1.7" }, "bin": { "govuk-prototype-kit": "bin/cli" @@ -11632,40 +11631,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/universal-analytics": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", - "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.1", - "uuid": "^8.0.0" - }, - "engines": { - "node": ">=12.18.2" - } - }, - "node_modules/universal-analytics/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/universal-analytics/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -11754,6 +11719,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -20417,30 +20383,6 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, - "universal-analytics": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", - "integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==", - "requires": { - "debug": "^4.3.1", - "uuid": "^8.0.0" - }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", @@ -20494,7 +20436,8 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true }, "v8-to-istanbul": { "version": "9.1.3", diff --git a/package.json b/package.json index 2ac0f9971f..14d140a1ac 100644 --- a/package.json +++ b/package.json @@ -82,8 +82,7 @@ "portscanner": "^2.2.0", "sass": "^1.89.2", "semver": "^7.7.2", - "tar-stream": "^3.1.7", - "universal-analytics": "^0.5.3" + "tar-stream": "^3.1.7" }, "devDependencies": { "cheerio": "^1.0.0-rc.12", From 41fe2e9d08d9d597a87dc411986777f18ec1f896 Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Fri, 19 Jun 2026 09:31:33 +0100 Subject: [PATCH 3/6] Remove tracking related functionality from tests --- __tests__/spec/sanity-checks.js | 2 +- __tests__/utils/index.js | 8 -------- cypress/scripts/run-starter-prototype.js | 2 +- scripts/create-prototype-and-run.js | 2 +- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/__tests__/spec/sanity-checks.js b/__tests__/spec/sanity-checks.js index b51b1d31e6..7729238fb2 100644 --- a/__tests__/spec/sanity-checks.js +++ b/__tests__/spec/sanity-checks.js @@ -25,7 +25,7 @@ const createKitTimeout = parseInt(process.env.CREATE_KIT_TIMEOUT || '90000', 10) */ describe('The Prototype Kit', () => { beforeAll(async () => { - await mkPrototype(tmpDir, { allowTracking: false, overwrite: true }) + await mkPrototype(tmpDir, { overwrite: true }) const { setPackagesCache } = require('../../lib/plugins/packages') setPackagesCache([{ diff --git a/__tests__/utils/index.js b/__tests__/utils/index.js index 6b6bc931b1..536e6bf2ec 100644 --- a/__tests__/utils/index.js +++ b/__tests__/utils/index.js @@ -42,9 +42,6 @@ function mkdtempSync () { * @param {Object} [options] * @param {string} [options.kitPath] - Path to the kit to use when creating prototype, if not provided uses mkReleaseArchive * @param {boolean} [options.overwrite] - Allow existing prototype to be overwritten (optional) - * @param {boolean} [options.allowTracking] - If undefined no usage-data-config.json is created (optional), - * if true a usage-data-config.json is created allowing tracking, - * if false a usage-data-config.json is crated disallowing tracking * @param {boolean} [options.npmInstallLinks] - Set value for npm config install-links (optional) * @param {string} [options.commandLineParameters] - Command line parameters (optional) * @returns {Promise} @@ -52,7 +49,6 @@ function mkdtempSync () { async function mkPrototype (prototypePath, { kitPath, overwrite = false, - allowTracking = undefined, npmInstallLinks = undefined, commandLineParameters = '' } = {}) { // TODO: Use kitPath if provided @@ -85,10 +81,6 @@ async function mkPrototype (prototypePath, { { cwd: repoDir, env: execEnv, stdio: 'inherit' } ) - if (allowTracking !== undefined) { - await fse.writeJson(path.join(prototypePath, 'usage-data-config.json'), { collectUsageData: !!allowTracking }) - } - process.stderr.write(`Kit creation took [${Math.round((Date.now() - startTime) / 100) / 10}] seconds\n`) } catch (error) { console.error(error.message) diff --git a/cypress/scripts/run-starter-prototype.js b/cypress/scripts/run-starter-prototype.js index 79667f1b40..b96845d0e3 100644 --- a/cypress/scripts/run-starter-prototype.js +++ b/cypress/scripts/run-starter-prototype.js @@ -10,7 +10,7 @@ const defaultKitPath = path.join(os.tmpdir(), 'cypress', 'test-prototype') const testDir = path.resolve(process.env.KIT_TEST_DIR || defaultKitPath) ;(async () => { - await mkPrototype(testDir, { overwrite: true, allowTracking: false, npmInstallLinks: true }) + await mkPrototype(testDir, { overwrite: true, npmInstallLinks: true }) const fooLocation = path.join(__dirname, '..', 'fixtures', 'plugins', 'plugin-foo') const barLocation = path.join(__dirname, '..', 'fixtures', 'plugins', 'plugin-bar') diff --git a/scripts/create-prototype-and-run.js b/scripts/create-prototype-and-run.js index 4bf46c1b76..59615f08e6 100644 --- a/scripts/create-prototype-and-run.js +++ b/scripts/create-prototype-and-run.js @@ -22,7 +22,7 @@ console.log(`running prototype using command "${command}"`) console.log() // noinspection JSVoidFunctionReturnValueUsed -mkPrototype(testDir, { overwrite: true, allowTracking: false }) +mkPrototype(testDir, { overwrite: true }) .then(() => { console.log() return exec(command, { cwd: testDir, env: { ...process.env, env: 'test' }, stdio: 'inherit' }) From 0d1bee61d93c83e1fa7a88895a2dd37fb3446116 Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Fri, 19 Jun 2026 09:33:39 +0100 Subject: [PATCH 4/6] Remove usage config from new prototypes .gitignore --- bin/cli | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/cli b/bin/cli index eac1e773b9..2d82bcfe95 100755 --- a/bin/cli +++ b/bin/cli @@ -77,7 +77,6 @@ node_modules/ .tmp/ .env migrate.log -usage-data-config.json # General ignores .DS_Store From b4db6d39b87805d7e6bdc859c8619d230e76484e Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Fri, 19 Jun 2026 09:34:42 +0100 Subject: [PATCH 5/6] Clean up other places we write usage config --- scripts/performance-test-prepare | 1 - scripts/refresh-companion-kit | 1 - 2 files changed, 2 deletions(-) diff --git a/scripts/performance-test-prepare b/scripts/performance-test-prepare index fa2d6f5689..1c01603582 100755 --- a/scripts/performance-test-prepare +++ b/scripts/performance-test-prepare @@ -18,7 +18,6 @@ cd tmp/performance-testing rm -Rf govuk-prototype-kit-performance-kit git clone git@github.com:alphagov/govuk-prototype-kit-performance-kit.git cd govuk-prototype-kit-performance-kit -echo '{"collectUsageData": false}' > usage-data-config.json npm install $dependency --save-exact npm install npm run dev diff --git a/scripts/refresh-companion-kit b/scripts/refresh-companion-kit index 21ce259f46..11b9e6063f 100755 --- a/scripts/refresh-companion-kit +++ b/scripts/refresh-companion-kit @@ -1,4 +1,3 @@ #!/bin/sh mkdir -p ~/projects/prototype-kits/ && rm -Rf ~/projects/prototype-kits/companion-prototype-kit && ./bin/cli create --version=local-symlink ~/projects/prototype-kits/companion-prototype-kit --use-njk-extensions -echo '{"collectUsageData": false}' > ~/projects/prototype-kits/companion-prototype-kit/usage-data-config.json From 919b985fdc9dff693751e5966645d858ba94f984 Mon Sep 17 00:00:00 2001 From: Oliver Byford Date: Mon, 22 Jun 2026 09:23:00 +0100 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2c091803..bbb9ad79ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - [#2539: Remove dependency on `sync-request`](https://github.com/alphagov/govuk-prototype-kit/pull/2539) - [#2540: Remove dependency on `require-dir`](https://github.com/alphagov/govuk-prototype-kit/pull/2540) - [#2543: Update `qs`, `ws`, `body-parser`, `express`, `engine.io`, `engine.io-client` and `socket.io-adapter`](https://github.com/alphagov/govuk-prototype-kit/pull/2543) +- [#2545: Remove usage data tracking](https://github.com/alphagov/govuk-prototype-kit/pull/2545) ## 13.20.1