diff --git a/.gitignore b/.gitignore
index e43f79dbf0..f2b7a43bdb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+# ignore tracking usage data config - we want a different one for each user
+usage-data-config.json
.env
.sass-cache
.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6e1b90733..cd5e2befbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ New features:
- [#502 Add Cookies and Privacy policy text](https://github.com/alphagov/govuk_prototype_kit/pull/502)
- [#521 Do not track users who have enabled 'DoNotTrack'](https://github.com/alphagov/govuk_prototype_kit/pull/521)
- [#522 Add inline-code block styles](https://github.com/alphagov/govuk_prototype_kit/pull/522)
+- [#523 Track app usage](https://github.com/alphagov/govuk_prototype_kit/pull/523)
Bug fixes:
diff --git a/docs/documentation/usage-data.md b/docs/documentation/usage-data.md
new file mode 100644
index 0000000000..180139cba9
--- /dev/null
+++ b/docs/documentation/usage-data.md
@@ -0,0 +1,29 @@
+# Collecting usage data
+
+You can choose to have the Prototype Kit send anonymous usage data for analysis.
+This helps the team working on the Kit understand how it's being used, in order
+to improve it - please don't turn off usage data unless you have to.
+
+## How it works
+
+When you first run the Prototype Kit, it will ask you for permission to send
+usage data to Google Analytics. It will store your answer in `usage-data-config.json` and it won't ask
+you again.
+
+If you say yes, it will store an anonymous, unique ID number in `usage-data-config.json`.
+
+## Data we collect
+
+The kit will only send data when you run it on your computer. It does not send data when you run it on Heroku.
+
+Whenever you start the Prototype Kit, it will send:
+
+ - your anonymous ID number
+ - the Prototype Kit version number
+ - your operating system (for example 'Windows 10')
+ - your Node.js version
+
+## Change usage data settings
+
+You can start or stop sending usage data at any time. Delete `usage-data-config.json`
+and restart the Prototype Kit. It will ask you again whether you'd like to send data.
diff --git a/docs/views/tutorials-and-examples.html b/docs/views/tutorials-and-examples.html
index 151b063750..7e3e165695 100644
--- a/docs/views/tutorials-and-examples.html
+++ b/docs/views/tutorials-and-examples.html
@@ -57,6 +57,9 @@
Getting started
Basic usage
+ -
+ Sending kit usage data
+
-
Making pages
diff --git a/lib/usage-data-prompt.txt b/lib/usage-data-prompt.txt
new file mode 100644
index 0000000000..f30221456d
--- /dev/null
+++ b/lib/usage-data-prompt.txt
@@ -0,0 +1,9 @@
+
+Help us improve the GOV.UK Prototype Kit
+────────────────────────────────────────
+
+With your permission, the kit can send useful anonymous usage data
+for analysis to help the team improve the service. Read more here:
+https://govuk-prototype-kit.herokuapp.com/docs/usage-data
+
+Do you give permission for the kit to send anonymous usage data? (y/n)
diff --git a/lib/usage_data.js b/lib/usage_data.js
new file mode 100644
index 0000000000..898e765e43
--- /dev/null
+++ b/lib/usage_data.js
@@ -0,0 +1,95 @@
+// Core dependencies
+const path = require('path')
+const fs = require('fs')
+const os = require('os')
+
+// NPM dependencies
+const prompt = require('prompt')
+const universalAnalytics = require('universal-analytics')
+const uuidv4 = require('uuid/v4')
+
+// Local dependencies
+const packageJson = require('../package.json')
+
+exports.getUsageDataConfig = function () {
+ // Try to read config file to see if usage data is opted in
+ let usageDataConfig = {}
+ try {
+ usageDataConfig = require(path.join(__dirname, '../usage-data-config.json'))
+ } catch (e) {
+ // do nothing - we will make a config
+ }
+ return usageDataConfig
+}
+
+exports.setUsageDataConfig = function (usageDataConfig) {
+ const usageDataConfigJSON = JSON.stringify(usageDataConfig, null, ' ')
+ try {
+ fs.writeFileSync(path.join(__dirname, '../usage-data-config.json'), usageDataConfigJSON)
+ return true
+ } catch (error) {
+ console.error(error)
+ }
+ return false
+}
+
+// Ask for permission to track data
+// returns a Promise with the user's answer
+exports.askForUsageDataPermission = function () {
+ return new Promise(function (resolve, reject) {
+ // Set up prompt settings
+ prompt.colors = false
+ prompt.start()
+ prompt.message = ''
+ prompt.delimiter = ''
+
+ const description = fs.readFileSync(path.join(__dirname, 'usage-data-prompt.txt'), 'utf8')
+
+ prompt.get([{
+ name: 'answer',
+ description: description,
+ required: true,
+ type: 'string',
+ pattern: /y(es)?|no?/i,
+ message: 'Please enter y or n'
+ }], function (err, result) {
+ if (err) {
+ reject(err)
+ }
+ if (result.answer.match(/y(es)?/i)) {
+ resolve('yes')
+ } else {
+ resolve('no')
+ }
+ })
+ })
+}
+
+exports.startTracking = function (usageDataConfig) {
+ // Get client ID for tracking
+ // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters#cid
+
+ if (usageDataConfig.clientId === undefined) {
+ usageDataConfig.clientId = uuidv4()
+ 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()
+}
diff --git a/package.json b/package.json
index 93c774851c..bfe6b9a31a 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,9 @@
"run-sequence": "^1.2.2",
"standard": "^10.0.2",
"supertest": "^3.0.0",
- "sync-request": "^4.0.3"
+ "sync-request": "^4.0.3",
+ "universal-analytics": "^0.4.16",
+ "uuid": "^3.2.1"
},
"greenkeeper": {
"ignore": [
diff --git a/start.js b/start.js
index 315e446fde..cbe009c677 100644
--- a/start.js
+++ b/start.js
@@ -2,19 +2,53 @@
const path = require('path')
const fs = require('fs')
-// Warn if node_modules folder doesn't exist
-const nodeModulesExists = fs.existsSync(path.join(__dirname, '/node_modules'))
-if (!nodeModulesExists) {
- console.error('ERROR: Node module folder missing. Try running `npm install`')
- process.exit(0)
+checkFiles()
+
+// Local dependencies
+const usageData = require('./lib/usage_data')
+
+// Get usageDataConfig from file, if exists
+const usageDataConfig = usageData.getUsageDataConfig()
+
+if (usageDataConfig.collectUsageData === undefined) {
+ // No recorded answer, so ask for permission
+ let promptPromise = usageData.askForUsageDataPermission()
+ promptPromise.then(function (answer) {
+ if (answer === 'yes') {
+ usageDataConfig.collectUsageData = true
+ usageData.setUsageDataConfig(usageDataConfig)
+ usageData.startTracking(usageDataConfig)
+ } else if (answer === 'no') {
+ usageDataConfig.collectUsageData = false
+ usageData.setUsageDataConfig(usageDataConfig)
+ } else {
+ console.error(answer)
+ }
+ runGulp()
+ })
+} else if (usageDataConfig.collectUsageData === true) {
+ // Opted in
+ usageData.startTracking(usageDataConfig)
+ runGulp()
+} else {
+ // Opted out
+ runGulp()
}
-// Create template .env file if it doesn't exist
-const envExists = fs.existsSync(path.join(__dirname, '/.env'))
-if (!envExists) {
- console.log('Creating template .env file')
- fs.createReadStream(path.join(__dirname, '/lib/template.env'))
- .pipe(fs.createWriteStream(path.join(__dirname, '/.env')))
+// Warn if node_modules folder doesn't exist
+function checkFiles () {
+ const nodeModulesExists = fs.existsSync(path.join(__dirname, '/node_modules'))
+ if (!nodeModulesExists) {
+ console.error('ERROR: Node module folder missing. Try running `npm install`')
+ process.exit(0)
+ }
+
+ // Create template .env file if it doesn't exist
+ const envExists = fs.existsSync(path.join(__dirname, '/.env'))
+ if (!envExists) {
+ fs.createReadStream(path.join(__dirname, '/lib/template.env'))
+ .pipe(fs.createWriteStream(path.join(__dirname, '/.env')))
+ }
}
// Create template session data defaults file if it doesn't exist
@@ -33,14 +67,16 @@ if (!sessionDataDefaultsFileExists) {
}
// Run gulp
-const spawn = require('cross-spawn')
+function runGulp () {
+ const spawn = require('cross-spawn')
-process.env['FORCE_COLOR'] = 1
-var gulp = spawn('gulp')
-gulp.stdout.pipe(process.stdout)
-gulp.stderr.pipe(process.stderr)
-process.stdin.pipe(gulp.stdin)
+ process.env['FORCE_COLOR'] = 1
+ var gulp = spawn('gulp')
+ gulp.stdout.pipe(process.stdout)
+ gulp.stderr.pipe(process.stderr)
+ process.stdin.pipe(gulp.stdin)
-gulp.on('exit', function (code) {
- console.log('gulp exited with code ' + code.toString())
-})
+ gulp.on('exit', function (code) {
+ console.log('gulp exited with code ' + code.toString())
+ })
+}