diff --git a/doc/cordova.txt b/doc/cordova.txt index 00e75728..3cf5b630 100644 --- a/doc/cordova.txt +++ b/doc/cordova.txt @@ -5,7 +5,6 @@ Synopsis Global Commands create ............................. Create a project help ............................... Get help for a command - telemetry .......................... Turn telemetry collection on or off config ............................. Set, get, delete, edit, and list global cordova options Project Commands diff --git a/doc/readme.md b/doc/readme.md index 119a7d69..2c6fff27 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -40,7 +40,6 @@ These commands are available at all times. |----------|-------------- | `create` | Create a project | `help ` | Get help for a command -| `telemetry` | Turn telemetry collection on or off | `config` | Set, get, delete, edit, and list global cordova options ## Project Command List @@ -69,7 +68,6 @@ These options apply to all cordova-cli commands. | -d or --verbose | Pipe out more verbose output to your shell. You can also subscribe to `log` and `warn` events if you are consuming `cordova-cli` as a node module by calling `cordova.on('log', function() {})` or `cordova.on('warn', function() {})`. | -v or --version | Print out the version of your `cordova-cli` install. |--nohooks | Suppress executing hooks (taking RegExp hook patterns as parameters) -| --no-telemetry | Disable telemetry collection for the current command. ## Platform-specific options @@ -616,38 +614,6 @@ Run a local web server for www/ assets using specified `port` or default of 8000 cordova serve [port] ``` -## `cordova telemetry` command - -Turns telemetry collection on or off. - -**Command Syntax:** - -```bash -cordova telemetry [State] -``` - -| State | Description -|-------------|------------------ -| on | Turn telemetry collection on. -| off | Turn telemetry collection off. - -### Details - -A timed prompt asking the user to opt-in or out is displayed the first time cordova is run. -It lasts for 30 seconds, after which the user is automatically opted-out if they don't provide any answer. -In CI environments, the `CI` environment variable can be set, which will prevent the prompt from showing up. -Telemetry collection can also be turned off on a single command by using the `--no-telemetry` flag. - -**Usage Example:** - -```bash -cordova telemetry on -cordova telemetry off -cordova build --no-telemetry -``` - -For details, see our privacy notice: https://cordova.apache.org/privacy - ## `cordova help` command Show syntax summary, or the help for a specific command. diff --git a/doc/telemetry.txt b/doc/telemetry.txt deleted file mode 100644 index 84a90815..00000000 --- a/doc/telemetry.txt +++ /dev/null @@ -1,23 +0,0 @@ -Synopsis - - cordova-cli telemetry [STATE] - - STATE: on|off - -Turns telemetry collection on or off - - on ....................... Turns telemetry collection on - off ...................... Turns telemetry collection off - -Details - A timed prompt asking the user to opt-in or out is displayed the first time cordova is run. - It lasts for 30 seconds, after which the user is automatically opted-out if they don't provide any answer. - In CI environments, the `CI` environment variable can be set, which will prevent the prompt from showing up. - Telemetry collection can also be turned off on a single command by using the `--no-telemetry` flag. - -Examples - cordova-cli telemetry on - cordova-cli telemetry off - cordova-cli build --no-telemetry - -For details, see our privacy notice: https://cordova.apache.org/privacy \ No newline at end of file diff --git a/spec/cli.spec.js b/spec/cli.spec.js index 2edafcbe..59b5f6af 100644 --- a/spec/cli.spec.js +++ b/spec/cli.spec.js @@ -18,7 +18,6 @@ const path = require('path'); const rewire = require('rewire'); const { events, cordova } = require('cordova-lib'); -const telemetry = require('../src/telemetry'); describe('cordova cli', () => { let cli, logger; @@ -41,12 +40,6 @@ describe('cordova cli', () => { ]); cli.__set__({ logger }); spyOn(console, 'log'); - - // Prevent accidentally turning telemetry on/off during testing - spyOn(telemetry, 'track'); - spyOn(telemetry, 'turnOn'); - spyOn(telemetry, 'turnOff'); - spyOn(telemetry, 'showPrompt').and.returnValue(Promise.resolve()); }); describe('options', () => { @@ -247,102 +240,6 @@ describe('cordova cli', () => { }); }); - describe('telemetry', () => { - const Insight = require('insight'); - let isOptedOut; - - beforeEach(() => { - // Allow testing if we _really_ would send tracking requests - telemetry.track.and.callThrough(); - telemetry.turnOn.and.callThrough(); - telemetry.turnOff.and.callThrough(); - spyOn(Insight.prototype, 'track').and.callThrough(); - spyOn(Insight.prototype, '_save'); - spyOnProperty(Insight.prototype, 'optOut', 'get') - .and.callFake(() => isOptedOut); - spyOnProperty(Insight.prototype, 'optOut', 'set') - .and.callFake(x => { isOptedOut = x; }); - - // Set a normal opted-in user as default - spyOn(telemetry, 'isCI').and.returnValue(false); - isOptedOut = false; - }); - - it("Test#023 : skips prompt when user runs 'cordova telemetry X'", () => { - isOptedOut = undefined; - - return Promise.resolve() - .then(_ => cli(['node', 'cordova', 'telemetry', 'on'])) - .then(_ => cli(['node', 'cordova', 'telemetry', 'off'])) - .then(() => { - expect(telemetry.showPrompt).not.toHaveBeenCalled(); - }); - }); - - it("Test#024 : is NOT collected when user runs 'cordova telemetry on' while NOT opted-in", () => { - isOptedOut = true; - - return cli(['node', 'cordova', 'telemetry', 'on']).then(() => { - expect(Insight.prototype.track).not.toHaveBeenCalled(); - }); - }); - - it("Test#025 : is collected when user runs 'cordova telemetry off' while opted-in", () => { - return cli(['node', 'cordova', 'telemetry', 'off']).then(() => { - expect(telemetry.track).toHaveBeenCalledWith('telemetry', 'off', 'via-cordova-telemetry-cmd', 'successful'); - expect(Insight.prototype.track).toHaveBeenCalled(); - expect(Insight.prototype._save).toHaveBeenCalled(); - }); - }); - - it('Test#026 : tracks platforms/plugins subcommands', () => { - spyOn(cordova, 'platform').and.returnValue(Promise.resolve()); - - return cli(['node', 'cordova', 'platform', 'add', 'ios']).then(() => { - expect(telemetry.track).toHaveBeenCalledWith('platform', 'add', 'successful'); - expect(Insight.prototype.track).toHaveBeenCalled(); - expect(Insight.prototype._save).toHaveBeenCalled(); - }); - }); - - it('Test#027 : shows prompt if user neither opted in or out yet', () => { - isOptedOut = undefined; - spyOn(cordova, 'prepare').and.returnValue(Promise.resolve()); - - return cli(['node', 'cordova', 'prepare']).then(() => { - expect(telemetry.showPrompt).toHaveBeenCalled(); - }); - }); - - it('Test#029 : is NOT collected in CI environments', () => { - telemetry.isCI.and.returnValue(true); - - return cli(['node', 'cordova', '--version']).then(() => { - expect(telemetry.track).not.toHaveBeenCalled(); - }); - }); - - it("Test#030 : is NOT collected when --no-telemetry flag found and doesn't prompt", () => { - isOptedOut = undefined; - - return cli(['node', 'cordova', '--version', '--no-telemetry']).then(() => { - expect(telemetry.showPrompt).not.toHaveBeenCalled(); - expect(Insight.prototype.track).not.toHaveBeenCalled(); - }); - }); - - it("Test#033 : track opt-out that happened via 'cordova telemetry off' even if user is NOT opted-in ", () => { - isOptedOut = true; - - return cli(['node', 'cordova', 'telemetry', 'off']).then(() => { - expect(telemetry.isOptedIn()).toBeFalsy(); - expect(telemetry.track).toHaveBeenCalledWith('telemetry', 'off', 'via-cordova-telemetry-cmd', 'successful'); - expect(Insight.prototype.track).toHaveBeenCalled(); - expect(Insight.prototype._save).toHaveBeenCalled(); - }); - }); - }); - describe('platform', () => { beforeEach(() => { spyOn(cordova, 'platform').and.returnValue(Promise.resolve()); diff --git a/spec/telemetry.spec.js b/spec/telemetry.spec.js deleted file mode 100644 index cb955135..00000000 --- a/spec/telemetry.spec.js +++ /dev/null @@ -1,297 +0,0 @@ -/*! - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -const rewire = require('rewire'); -const Insight = require('insight'); -const mockStdin = require('mock-stdin'); - -function restoreAfterEach (obj, prop) { - const originalValue = obj[prop]; - const restoreProp = Object.prototype.hasOwnProperty.call(obj, prop) - ? () => { obj[prop] = originalValue; } - : () => { delete obj[prop]; }; - - afterEach(restoreProp); -} - -describe('telemetry', () => { - let telemetry, insight; - - beforeEach(() => { - telemetry = rewire('../src/telemetry'); - insight = telemetry.__get__('insight'); - - // Prevent any settings from being persisted during testing - insight.config = { - get (key) { return this[key]; }, - set (key, val) { this[key] = val; } - }; - for (const key in insight.config) { - spyOn(insight.config, key).and.callThrough(); - } - - // Prevent tracking anything during testing - spyOn(Insight.prototype, '_save'); - - // Prevent prompts during testing - spyOn(Insight.prototype, 'askPermission'); - }); - - describe('hasUserOptedInOrOut', () => { - it('is false if insight.optOut is unset [T001]', () => { - expect(telemetry.hasUserOptedInOrOut()).toBe(false); - expect(insight.config.get).toHaveBeenCalledWith('optOut'); - }); - - it('is true if insight.optOut is set [T002]', () => { - insight.config.get.and.returnValues( - false, true, 0, 1, '', 'xxx', null - ); - for (let i = 0; i < 7; i++) { - expect(telemetry.hasUserOptedInOrOut()).toBe(true); - } - expect(insight.config.get).toHaveBeenCalledTimes(7); - }); - }); - - describe('isOptedIn', () => { - it('is the inverse of insight.optOut [T003]', () => { - insight.config.get.and.returnValues(false, true); - - expect(telemetry.isOptedIn()).toBe(true); - expect(telemetry.isOptedIn()).toBe(false); - expect(insight.config.get).toHaveBeenCalledTimes(2); - }); - - it('is true if user did not yet decide [T004]', () => { - expect(telemetry.isOptedIn()).toBe(true); - expect(insight.config.get).toHaveBeenCalledWith('optOut'); - }); - }); - - describe('clear', () => { - it('clears telemetry setting [T005]', () => { - telemetry.clear(); - expect(insight.config.set) - .toHaveBeenCalledWith('optOut', undefined); - }); - }); - - describe('turnOn', () => { - it('enables the telemetry setting [T006]', () => { - telemetry.turnOn(); - expect(insight.config.set) - .toHaveBeenCalledWith('optOut', false); - }); - }); - - describe('turnOff', () => { - it('disables the telemetry setting [T007]', () => { - telemetry.turnOff(); - expect(insight.config.set) - .toHaveBeenCalledWith('optOut', true); - }); - }); - - describe('track', () => { - beforeEach(() => { - spyOn(Insight.prototype, 'track'); - }); - - it('calls insight.track [T008]', () => { - telemetry.track(); - expect(insight.track).toHaveBeenCalled(); - }); - - it('passes its arguments to insight.track [T009]', () => { - const args = ['foo', 'bar', 42]; - telemetry.track(...args); - expect(insight.track).toHaveBeenCalledWith(...args); - }); - - it('filters falsy and empty arguments [T010]', () => { - const args = [null, [23], [], 42, '']; - telemetry.track(...args); - expect(insight.track).toHaveBeenCalledWith([23], 42); - }); - }); - - describe('showPrompt', () => { - let response; - - beforeEach(() => { - spyOn(console, 'log'); - spyOn(telemetry, 'track').and.callThrough(); - response = Symbol('response'); - insight.askPermission.and.callFake((_) => { - insight.optOut = !response; - return Promise.resolve(response); - }); - }); - - it('calls insight.askPermission [T011]', () => { - return telemetry.showPrompt().then(_ => { - expect(insight.askPermission).toHaveBeenCalled(); - }); - }); - - it('returns a promise resolved to the user response [T012]', () => { - return telemetry.showPrompt().then(result => { - expect(result).toBe(response); - }); - }); - - describe('when user opts in', () => { - beforeEach(() => { - response = true; - }); - - it('thanks the user [T013]', () => { - return telemetry.showPrompt().then(_ => { - expect(console.log).toHaveBeenCalledWith( - jasmine.stringMatching(/thanks/i) - ); - }); - }); - - it('tracks the user decision [T014]', () => { - return telemetry.showPrompt().then(_ => { - expect(telemetry.track).toHaveBeenCalledWith( - 'telemetry', 'on', 'via-cli-prompt-choice', 'successful' - ); - expect(Insight.prototype._save).toHaveBeenCalled(); - }); - }); - }); - - describe('when user declines', () => { - beforeEach(() => { - response = false; - }); - - it('returns a resolved promise if the user response was negative [T015]', () => { - return telemetry.showPrompt().then(result => { - expect(result).toBe(false); - }); - }); - - it('informs the user [T016]', () => { - return telemetry.showPrompt().then(_ => { - expect(console.log).toHaveBeenCalledWith( - jasmine.stringMatching(/opted out of telemetry.* cordova telemetry on/i) - ); - }); - }); - - it('tracks the user decision [T017]', () => { - return telemetry.showPrompt().then(_ => { - expect(telemetry.track).toHaveBeenCalledWith( - 'telemetry', 'off', 'via-cli-prompt-choice', 'successful' - ); - expect(Insight.prototype._save).toHaveBeenCalled(); - }); - }); - }); - - describe('gory details', () => { - let stdin; - beforeEach(() => { - // Ensure that insight really shows a prompt - delete process.env.CI; - process.stdout.isTTY = true; - insight.askPermission.and.callThrough(); - - stdin = mockStdin.stdin(); - - // To silence the prompts by insight - spyOn(process.stdout, 'write'); - - // Ensure that prompts are shown for 10ms at most - telemetry.timeoutInSecs = 0.01; - }); - afterEach(() => { - stdin.restore(); - }); - restoreAfterEach(process.env, 'CI'); - restoreAfterEach(process.stdout, 'isTTY'); - - it('actually shows a prompt [T025]', () => { - return telemetry.showPrompt().then(() => { - expect(process.stdout.write).toHaveBeenCalled(); - }); - }); - - it('saves the user response [T018]', () => { - process.nextTick(_ => stdin.send('y\n')); - return telemetry.showPrompt().then(result => { - expect(result).toBe(true); - expect(insight.config.set) - .toHaveBeenCalledWith('optOut', false); - }); - }); - - it('is counted as a negative response if user does not decide [T019]', () => { - return telemetry.showPrompt().then(result => { - expect(result).toBe(false); - expect(insight.config.set) - .toHaveBeenCalledWith('optOut', true); - }); - }); - - it('does NOT show prompt when running on a CI [T020]', () => { - process.env.CI = 1; - return telemetry.showPrompt().then(result => { - expect(result).toBe(undefined); - expect(insight.config.set).not.toHaveBeenCalled(); - expect(process.stdout.write).not.toHaveBeenCalled(); - }); - }); - }); - }); - describe('insight.track', () => { - it('tracks without user choice [T021]', () => { - insight.track(); - expect(insight._save).toHaveBeenCalled(); - }); - - it('tracks with user consent [T022]', () => { - insight.config.get.and.returnValue(false); - insight.track(); - expect(insight._save).toHaveBeenCalled(); - }); - - it('still tracks when user opted out [T023]', () => { - insight.config.get.and.returnValue(true); - insight.track(); - expect(insight._save).toHaveBeenCalled(); - }); - - describe('on CI', () => { - beforeEach(() => { - process.env.CI = 1; - }); - restoreAfterEach(process.env, 'CI'); - - it('does still track [T024]', () => { - insight.track(); - expect(insight._save).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/src/cli.js b/src/cli.js index 735ed143..fbf8e0f0 100644 --- a/src/cli.js +++ b/src/cli.js @@ -17,7 +17,6 @@ const nopt = require('nopt'); const pkg = require('../package.json'); -const telemetry = require('./telemetry'); const help = require('./help'); const info = require('./info'); const cordova_lib = require('cordova-lib'); @@ -75,14 +74,10 @@ const shortHands = { t: '--template' }; -let shouldCollectTelemetry = false; - module.exports = function (inputArgs) { // If no inputArgs given, use process.argv. inputArgs = inputArgs || process.argv; let cmd = inputArgs[2]; // e.g: inputArgs= 'node cordova run ios' - const subcommand = getSubCommand(inputArgs, cmd); - const isTelemetryCmd = (cmd === 'telemetry'); const isConfigCmd = (cmd === 'config'); // ToDO: Move nopt-based parsing of args up here @@ -136,103 +131,15 @@ module.exports = function (inputArgs) { } return Promise.resolve().then(function () { - /** - * Skip telemetry prompt if: - * - CI environment variable is present - * - Command is run with `--no-telemetry` flag - * - Command ran is: `cordova telemetry on | off | ...` - */ - - if (telemetry.isCI(process.env) || telemetry.isNoTelemetryFlag(inputArgs)) { - return Promise.resolve(false); - } - - /** - * We shouldn't prompt for telemetry if user issues a command of the form: `cordova telemetry on | off | ...x` - * Also, if the user has already been prompted and made a decision, use his saved answer - */ - if (isTelemetryCmd) { - const isOptedIn = telemetry.isOptedIn(); - return handleTelemetryCmd(subcommand, isOptedIn); - } - - if (telemetry.hasUserOptedInOrOut()) { - return Promise.resolve(telemetry.isOptedIn()); - } - - /** - * Otherwise, prompt user to opt-in or out - * Note: the prompt is shown for 30 seconds. If no choice is made by that time, User is considered to have opted out. - */ - return telemetry.showPrompt(); - }).then(function (collectTelemetry) { - shouldCollectTelemetry = collectTelemetry; - if (isTelemetryCmd) { - return Promise.resolve(); - } return cli(inputArgs); - }).then(function () { - if (shouldCollectTelemetry && !isTelemetryCmd) { - telemetry.track(cmd, subcommand, 'successful'); - } - }).catch(function (err) { - if (shouldCollectTelemetry && !isTelemetryCmd) { - telemetry.track(cmd, subcommand, 'unsuccessful'); - } - throw err; }); }; -function getSubCommand (args, cmd) { - if (['platform', 'platforms', 'plugin', 'plugins', 'telemetry', 'config'].indexOf(cmd) > -1) { - return args[3]; // e.g: args='node cordova platform rm ios', 'node cordova telemetry on' - } - return null; -} - function printHelp (command) { const result = help([command]); cordova.emit('results', result); } -function handleTelemetryCmd (subcommand, isOptedIn) { - if (subcommand !== 'on' && subcommand !== 'off') { - logger.subscribe(events); - printHelp('telemetry'); - return; - } - - const turnOn = subcommand === 'on'; - let cmdSuccess = true; - - // turn telemetry on or off - try { - if (turnOn) { - telemetry.turnOn(); - console.log('Thanks for opting into telemetry to help us improve cordova.'); - } else { - telemetry.turnOff(); - console.log('You have been opted out of telemetry. To change this, run: cordova telemetry on.'); - } - } catch (ex) { - cmdSuccess = false; - } - - // track or not track ?, that is the question - - if (!turnOn) { - // Always track telemetry opt-outs (whether user opted out or not!) - telemetry.track('telemetry', 'off', 'via-cordova-telemetry-cmd', cmdSuccess ? 'successful' : 'unsuccessful'); - return Promise.resolve(); - } - - if (isOptedIn) { - telemetry.track('telemetry', 'on', 'via-cordova-telemetry-cmd', cmdSuccess ? 'successful' : 'unsuccessful'); - } - - return Promise.resolve(); -} - function cli (inputArgs) { const args = nopt(knownOpts, shortHands, inputArgs); @@ -242,10 +149,6 @@ function cli (inputArgs) { } else { logger.error(err); } - // Don't send exception details, just send that it happened - if (shouldCollectTelemetry) { - telemetry.track('uncaughtException'); - } process.exit(1); }); diff --git a/src/telemetry.js b/src/telemetry.js deleted file mode 100644 index ec6f3cc8..00000000 --- a/src/telemetry.js +++ /dev/null @@ -1,129 +0,0 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -// For further details on telemetry, see: -// https://github.com/cordova/cordova-discuss/pull/43 - -const { EOL } = require('os'); - -// Google Analytics tracking code -const GA_TRACKING_CODE = 'UA-64283057-7'; - -const pkg = require('../package.json'); -const Insight = require('insight'); - -/** - * By redefining `get optOut` we trick Insight into tracking - * even though the user might have opted out. - */ -class RelentlessInsight extends Insight { - get optOut () { return false; } - - set optOut (value) { super.optOut = value; } - - get realOptOut () { return super.optOut; } -} - -const insight = new RelentlessInsight({ - trackingCode: GA_TRACKING_CODE, - pkg -}); - -/** - * Returns true if the user opted in, and false otherwise - */ -function showPrompt () { - insight._permissionTimeout = module.exports.timeoutInSecs || 30; - - const msg = 'May Cordova anonymously report usage statistics to improve the tool over time?'; - - return insight.askPermission(msg).then(optIn => { - if (optIn) { - console.log(EOL + 'Thanks for opting into telemetry to help us improve cordova.'); - module.exports.track('telemetry', 'on', 'via-cli-prompt-choice', 'successful'); - } else { - console.log(EOL + 'You have been opted out of telemetry. To change this, run: cordova telemetry on.'); - // Always track telemetry opt-outs! (whether opted-in or opted-out) - module.exports.track('telemetry', 'off', 'via-cli-prompt-choice', 'successful'); - } - - return optIn; - }); -} - -function track (...args) { - // Remove empty, null or undefined strings from arguments - const filteredArgs = args.filter(val => val && val.length !== 0); - insight.track(...filteredArgs); -} - -function turnOn () { - insight.optOut = false; -} - -function turnOff () { - insight.optOut = true; -} - -/** - * Clears telemetry setting - * Has the same effect as if user never answered the telemetry prompt - * Useful for testing purposes - */ -function clear () { - insight.optOut = undefined; -} - -function isOptedIn () { - return !insight.realOptOut; -} - -/** - * Has the user already answered the telemetry prompt? (thereby opting in or out?) - */ -function hasUserOptedInOrOut () { - const insightOptOut = insight.realOptOut === undefined; - return !(insightOptOut); -} - -/** - * Is the environment variable 'CI' specified ? - */ -function isCI (env) { - return !!env.CI; -} - -/** - * Has the user ran a command of the form: `cordova run --no-telemetry` ? - */ -function isNoTelemetryFlag (args) { - return args.indexOf('--no-telemetry') > -1; -} - -// this is to help testing, so we don't have to wait for the full 30 -module.exports = { - track, - turnOn, - turnOff, - clear, - isOptedIn, - hasUserOptedInOrOut, - isCI, - showPrompt, - isNoTelemetryFlag, - timeoutInSecs: 30 -};