From f0caf9eebdcb2fa785a2282f25b000efd88d7db2 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Tue, 5 Jan 2021 19:41:00 -0800 Subject: [PATCH 1/7] wip: telemetry --- cli/package.json | 1 + cli/src/index.ts | 109 +++++++++++++++++++++----------- cli/src/ipc.ts | 88 ++++++++++++++++++++++++++ cli/src/sysconfig.ts | 51 +++++++++++++++ cli/src/tasks/telemetry.ts | 46 ++++++++++++++ cli/src/telemetry.ts | 124 +++++++++++++++++++++++++++++++++++++ cli/src/util/cli.ts | 4 ++ cli/src/util/uuid.ts | 8 +++ 8 files changed, 396 insertions(+), 35 deletions(-) create mode 100644 cli/src/ipc.ts create mode 100644 cli/src/sysconfig.ts create mode 100644 cli/src/tasks/telemetry.ts create mode 100644 cli/src/telemetry.ts create mode 100644 cli/src/util/uuid.ts diff --git a/cli/package.json b/cli/package.json index 83901d6962..a6ea3fc8b5 100644 --- a/cli/package.json +++ b/cli/package.json @@ -50,6 +50,7 @@ "@ionic/utils-terminal": "^2.3.0", "commander": "^6.0.0", "debug": "^4.2.0", + "env-paths": "^2.2.0", "kleur": "^4.1.1", "native-run": "^1.2.1", "open": "^7.1.0", diff --git a/cli/src/index.ts b/cli/src/index.ts index 403f161e20..d84237ba7b 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -4,7 +4,9 @@ import c from './colors'; import { loadConfig } from './config'; import type { Config } from './definitions'; import { fatal, isFatal } from './errors'; +import { receive } from './ipc'; import { logger, output } from './log'; +import { telemetryAction } from './telemetry'; import { wrapAction } from './util/cli'; import { emoji as _e } from './util/emoji'; @@ -12,6 +14,8 @@ process.on('unhandledRejection', error => { console.error(c.failure('[fatal]'), error); }); +process.on('message', receive); + export async function run(): Promise { try { const config = await loadConfig(); @@ -54,10 +58,12 @@ export function runProgram(config: Config): void { 'Optional: Directory of your projects built web assets', ) .action( - wrapAction(async (appName, appId, { webDir }) => { - const { initCommand } = await import('./tasks/init'); - await initCommand(config, appName, appId, webDir); - }), + wrapAction( + telemetryAction(config, async (appName, appId, { webDir }) => { + const { initCommand } = await import('./tasks/init'); + await initCommand(config, appName, appId, webDir); + }), + ), ); program @@ -78,10 +84,12 @@ export function runProgram(config: Config): void { "Optional: if provided, Podfile.lock won't be deleted and pod install will use --deployment option", ) .action( - wrapAction(async (platform, { deployment }) => { - const { syncCommand } = await import('./tasks/sync'); - await syncCommand(config, platform, deployment); - }), + wrapAction( + telemetryAction(config, async (platform, { deployment }) => { + const { syncCommand } = await import('./tasks/sync'); + await syncCommand(config, platform, deployment); + }), + ), ); program @@ -96,20 +104,24 @@ export function runProgram(config: Config): void { "Optional: if provided, Podfile.lock won't be deleted and pod install will use --deployment option", ) .action( - wrapAction(async (platform, { deployment }) => { - const { updateCommand } = await import('./tasks/update'); - await updateCommand(config, platform, deployment); - }), + wrapAction( + telemetryAction(config, async (platform, { deployment }) => { + const { updateCommand } = await import('./tasks/update'); + await updateCommand(config, platform, deployment); + }), + ), ); program .command('copy [platform]') .description('copies the web app build into the native app') .action( - wrapAction(async platform => { - const { copyCommand } = await import('./tasks/copy'); - await copyCommand(config, platform); - }), + wrapAction( + telemetryAction(config, async platform => { + const { copyCommand } = await import('./tasks/copy'); + await copyCommand(config, platform); + }), + ), ); program @@ -123,52 +135,79 @@ export function runProgram(config: Config): void { .option('--target ', 'use a specific target') .option('--no-sync', `do not run ${c.input('sync')}`) .action( - wrapAction(async (platform, { list, target, sync }) => { - const { runCommand } = await import('./tasks/run'); - await runCommand(config, platform, { list, target, sync }); - }), + wrapAction( + telemetryAction(config, async (platform, { list, target, sync }) => { + const { runCommand } = await import('./tasks/run'); + await runCommand(config, platform, { list, target, sync }); + }), + ), ); program .command('open [platform]') .description('opens the native project workspace (Xcode for iOS)') .action( - wrapAction(async platform => { - const { openCommand } = await import('./tasks/open'); - await openCommand(config, platform); - }), + wrapAction( + telemetryAction(config, async platform => { + const { openCommand } = await import('./tasks/open'); + await openCommand(config, platform); + }), + ), ); program .command('add [platform]') .description('add a native platform project') .action( - wrapAction(async platform => { - const { addCommand } = await import('./tasks/add'); - await addCommand(config, platform); - }), + wrapAction( + telemetryAction(config, async platform => { + const { addCommand } = await import('./tasks/add'); + await addCommand(config, platform); + }), + ), ); program .command('ls [platform]') .description('list installed Cordova and Capacitor plugins') .action( - wrapAction(async platform => { - const { listCommand } = await import('./tasks/list'); - await listCommand(config, platform); - }), + wrapAction( + telemetryAction(config, async platform => { + const { listCommand } = await import('./tasks/list'); + await listCommand(config, platform); + }), + ), ); program .command('doctor [platform]') .description('checks the current setup for common errors') .action( - wrapAction(async platform => { - const { doctorCommand } = await import('./tasks/doctor'); - await doctorCommand(config, platform); + wrapAction( + telemetryAction(config, async platform => { + const { doctorCommand } = await import('./tasks/doctor'); + await doctorCommand(config, platform); + }), + ), + ); + + program + .command('telemetry [on|off]', { hidden: true }) + .description('enable or disable telemetry') + .action( + wrapAction(async onOrOff => { + const { telemetryCommand } = await import('./tasks/telemetry'); + await telemetryCommand(onOrOff); }), ); + program + .command('📡', { hidden: true }) + .description('IPC receiver command') + .action(() => { + // no-op: IPC messages are received via `process.on('message')` + }); + program .command('plugin:generate', { hidden: true }) .description('start a new Capacitor plugin') diff --git a/cli/src/ipc.ts b/cli/src/ipc.ts new file mode 100644 index 0000000000..862354ed44 --- /dev/null +++ b/cli/src/ipc.ts @@ -0,0 +1,88 @@ +import { open, mkdirp } from '@ionic/utils-fs'; +import { fork } from '@ionic/utils-subprocess'; +import Debug from 'debug'; +import { request } from 'https'; +import { resolve } from 'path'; + +import type { TelemetryMessage } from './telemetry'; +import { ENV_PATHS } from './util/cli'; + +const debug = Debug('capacitor:ipc'); + +export interface TelemetryIPCMessage { + type: 'telemetry'; + data: TelemetryMessage; +} + +export type IPCMessage = TelemetryIPCMessage; + +/** + * Send an IPC message to a forked process. + */ +export async function send(msg: IPCMessage): Promise { + const dir = ENV_PATHS.log; + await mkdirp(dir); + const logPath = resolve(dir, 'ipc.log'); + + debug( + 'Sending %O IPC message to forked process (logs: %O)', + msg.type, + logPath, + ); + + const fd = await open(logPath, 'a'); + const p = fork(process.argv[1], ['📡'], { stdio: ['ignore', fd, fd, 'ipc'] }); + + p.send(msg); + p.disconnect(); + p.unref(); +} + +/** + * Receive and handle an IPC message. + * + * Assume minimal context and keep external dependencies to a minimum. + */ +export async function receive(msg: IPCMessage): Promise { + debug('Received %O IPC message', msg.type); + + if (msg.type === 'telemetry') { + const now = new Date().toISOString(); + const { data } = msg; + + // This request is only made if telemetry is on. + const req = request( + { + hostname: 'api-staging.ionicjs.com', // TODO + port: 443, + path: '/events/metrics', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, + response => { + debug( + 'Sent telemetry data to events service (status: %O)', + response.statusCode, + ); + + if (response.statusCode !== 204) { + response.on('data', chunk => { + debug( + 'Bad response from events service. Request body: %O', + chunk.toString(), + ); + }); + } + }, + ); + + const body = { + metrics: [data], + sent_at: now, + }; + + req.end(JSON.stringify(body)); + } +} diff --git a/cli/src/sysconfig.ts b/cli/src/sysconfig.ts new file mode 100644 index 0000000000..20200bdaa8 --- /dev/null +++ b/cli/src/sysconfig.ts @@ -0,0 +1,51 @@ +import { readJSON, writeJSON, mkdirp } from '@ionic/utils-fs'; +import Debug from 'debug'; +import { dirname, resolve } from 'path'; + +import { ENV_PATHS } from './util/cli'; +import { uuidv4 } from './util/uuid'; + +const debug = Debug('capacitor:sysconfig'); + +const SYSCONFIG_FILE = 'sysconfig.json'; +const SYSCONFIG_PATH = resolve(ENV_PATHS.config, SYSCONFIG_FILE); + +export interface SystemConfig { + /** + * A UUID that anonymously identifies this computer. + */ + readonly machine: string; + + /** + * Whether telemetry is enabled or not. + */ + readonly telemetry: boolean; +} + +export async function readConfig(): Promise { + debug('Reading from %O', SYSCONFIG_PATH); + + try { + return await readJSON(SYSCONFIG_PATH); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + + const sysconfig: SystemConfig = { + machine: uuidv4(), + telemetry: true, + }; + + await writeConfig(sysconfig); + + return sysconfig; + } +} + +export async function writeConfig(sysconfig: SystemConfig): Promise { + debug('Writing to %O', SYSCONFIG_PATH); + + await mkdirp(dirname(SYSCONFIG_PATH)); + await writeJSON(SYSCONFIG_PATH, sysconfig, { spaces: '\t' }); +} diff --git a/cli/src/tasks/telemetry.ts b/cli/src/tasks/telemetry.ts new file mode 100644 index 0000000000..437909aea3 --- /dev/null +++ b/cli/src/tasks/telemetry.ts @@ -0,0 +1,46 @@ +import c from '../colors'; +import { fatal } from '../errors'; +import { logger, logSuccess, output } from '../log'; +import { readConfig, writeConfig } from '../sysconfig'; +import { THANK_YOU } from '../telemetry'; + +export async function telemetryCommand(onOrOff?: string): Promise { + const sysconfig = await readConfig(); + const enabled = interpretEnabled(onOrOff); + + if (typeof enabled === 'boolean') { + if (sysconfig.telemetry === enabled) { + logger.info(`Telemetry is already ${c.strong(enabled ? 'on' : 'off')}`); + } else { + await writeConfig({ ...sysconfig, telemetry: enabled }); + logSuccess( + `You have ${c.strong(`opted ${enabled ? 'in' : 'out'}`)} ${ + enabled ? 'for' : 'of' + } telemetry on this machine.`, + ); + + if (enabled) { + output.write(THANK_YOU); + } + } + } else { + logger.info(`Telemetry is ${c.strong(sysconfig.telemetry ? 'on' : 'off')}`); + } +} + +function interpretEnabled(onOrOff?: string): boolean | undefined { + switch (onOrOff) { + case 'on': + return true; + case 'off': + return false; + case undefined: + return undefined; + } + + fatal( + `Argument must be ${c.strong('on')} or ${c.strong( + 'off', + )} (or left unspecified)`, + ); +} diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts new file mode 100644 index 0000000000..64ece50470 --- /dev/null +++ b/cli/src/telemetry.ts @@ -0,0 +1,124 @@ +import { Command } from 'commander'; + +import c from './colors'; +import type { Config } from './definitions'; +import { send } from './ipc'; +import { readConfig } from './sysconfig'; + +export const THANK_YOU = + `\nThank you for helping to make Capacitor better! 💖` + + `\nInformation about the data we collect is available on our website: ${c.strong( + 'https://capacitorjs.com/telemetry', + )}\n`; + +export interface TelemetryData { + app_id: string; + command: string; + arguments: string; + options: string; + duration: number; + error: string | null; + node_version: string; + os: string; +} + +export interface TelemetryMessage { + name: string; + timestamp: string; + session_id: string; + source: string; + value: TelemetryData; +} + +async function sendTelemetryData(data: TelemetryData): Promise { + const sysconfig = await readConfig(); + + if (sysconfig.telemetry) { + const message: TelemetryMessage = { + name: 'capacitor_cli_command', + timestamp: new Date().toISOString(), + session_id: sysconfig.machine, + source: 'capacitor_cli', + value: data, + }; + + await send({ type: 'telemetry', data: message }); + } +} + +type CommanderAction = (...args: any[]) => void | Promise; + +export function telemetryAction( + config: Config, + action: CommanderAction, +): CommanderAction { + return async (...actionArgs: any[]): Promise => { + const start = new Date(); + // This is how commanderjs works--the command object is either the last + // element or second to last if there are additional options (via `.allowUnknownOption()`) + const lastArg = actionArgs[actionArgs.length - 1]; + const cmd: Command = + lastArg instanceof Command ? lastArg : actionArgs[actionArgs.length - 2]; + const command = getFullCommandName(cmd); + let error: any; + + try { + await action(...actionArgs); + } catch (e) { + error = e; + } + + const end = new Date(); + const duration = end.getTime() - start.getTime(); + + const packages = Object.entries({ + ...config.app.package.devDependencies, + ...config.app.package.dependencies, + }); + + // Only collect packages in the capacitor org: + // https://www.npmjs.com/org/capacitor + const capacitorPackages = packages.filter(([k]) => + k.startsWith('@capacitor/'), + ); + + const versions = capacitorPackages.map(([k, v]) => [ + `${k.replace(/^@capacitor\//, '').replace(/-/g, '_')}_version`, + v, + ]); + + const data: TelemetryData = { + app_id: '', // TODO + command, + arguments: cmd.args.join(' '), + options: JSON.stringify(cmd.opts()), + duration, + error: error ? (error.message ? error.message : String(error)) : null, + node_version: process.version, + os: config.cli.os, + ...Object.fromEntries(versions), + }; + + await sendTelemetryData(data); + + if (error) { + throw error; + } + }; +} + +/** + * Walk through the command's parent tree and construct a space-separated name. + * + * Probably overkill because we don't have nested commands, but whatever. + */ +function getFullCommandName(cmd: Command): string { + const names: string[] = []; + + while (cmd.parent !== null) { + names.push(cmd.name()); + cmd = cmd.parent; + } + + return names.reverse().join(' '); +} diff --git a/cli/src/util/cli.ts b/cli/src/util/cli.ts index 58f2cf2054..1cec6b31c7 100644 --- a/cli/src/util/cli.ts +++ b/cli/src/util/cli.ts @@ -1,6 +1,10 @@ +import envPaths from 'env-paths'; + import { isFatal } from '../errors'; import { logger } from '../log'; +export const ENV_PATHS = envPaths('capacitor', { suffix: '' }); + export type CommanderAction = (...args: any[]) => void | Promise; export function wrapAction(action: CommanderAction): CommanderAction { diff --git a/cli/src/util/uuid.ts b/cli/src/util/uuid.ts new file mode 100644 index 0000000000..9e66b50f06 --- /dev/null +++ b/cli/src/util/uuid.ts @@ -0,0 +1,8 @@ +export function uuidv4(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c == 'x' ? r : (r & 0x3) | 0x8; + + return v.toString(16); + }); +} From fa75e1a452406da88c7262d086af294c3172e96d Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Tue, 5 Jan 2021 19:42:55 -0800 Subject: [PATCH 2/7] interactive only --- cli/src/telemetry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index 64ece50470..e17ef9379e 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -4,6 +4,7 @@ import c from './colors'; import type { Config } from './definitions'; import { send } from './ipc'; import { readConfig } from './sysconfig'; +import { isInteractive } from './util/term'; export const THANK_YOU = `\nThank you for helping to make Capacitor better! 💖` + @@ -33,7 +34,7 @@ export interface TelemetryMessage { async function sendTelemetryData(data: TelemetryData): Promise { const sysconfig = await readConfig(); - if (sysconfig.telemetry) { + if (sysconfig.telemetry && isInteractive()) { const message: TelemetryMessage = { name: 'capacitor_cli_command', timestamp: new Date().toISOString(), From f89367bbacebfe37dbde7085a581f1a2e8367af3 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 6 Jan 2021 11:15:03 -0800 Subject: [PATCH 3/7] small refactor --- cli/src/ipc.ts | 7 ++++--- cli/src/telemetry.ts | 46 ++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/cli/src/ipc.ts b/cli/src/ipc.ts index 862354ed44..fc62f8801e 100644 --- a/cli/src/ipc.ts +++ b/cli/src/ipc.ts @@ -4,14 +4,14 @@ import Debug from 'debug'; import { request } from 'https'; import { resolve } from 'path'; -import type { TelemetryMessage } from './telemetry'; +import type { Metric } from './telemetry'; import { ENV_PATHS } from './util/cli'; const debug = Debug('capacitor:ipc'); export interface TelemetryIPCMessage { type: 'telemetry'; - data: TelemetryMessage; + data: Metric; } export type IPCMessage = TelemetryIPCMessage; @@ -63,7 +63,8 @@ export async function receive(msg: IPCMessage): Promise { }, response => { debug( - 'Sent telemetry data to events service (status: %O)', + 'Sent %O metric to events service (status: %O)', + data.name, response.statusCode, ); diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index e17ef9379e..b5ec791353 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -12,7 +12,7 @@ export const THANK_YOU = 'https://capacitorjs.com/telemetry', )}\n`; -export interface TelemetryData { +export interface CommandMetricData { app_id: string; command: string; arguments: string; @@ -23,28 +23,12 @@ export interface TelemetryData { os: string; } -export interface TelemetryMessage { - name: string; +export interface Metric { + name: N; timestamp: string; session_id: string; - source: string; - value: TelemetryData; -} - -async function sendTelemetryData(data: TelemetryData): Promise { - const sysconfig = await readConfig(); - - if (sysconfig.telemetry && isInteractive()) { - const message: TelemetryMessage = { - name: 'capacitor_cli_command', - timestamp: new Date().toISOString(), - session_id: sysconfig.machine, - source: 'capacitor_cli', - value: data, - }; - - await send({ type: 'telemetry', data: message }); - } + source: 'capacitor_cli'; + value: D; } type CommanderAction = (...args: any[]) => void | Promise; @@ -88,7 +72,7 @@ export function telemetryAction( v, ]); - const data: TelemetryData = { + const data: CommandMetricData = { app_id: '', // TODO command, arguments: cmd.args.join(' '), @@ -100,7 +84,7 @@ export function telemetryAction( ...Object.fromEntries(versions), }; - await sendTelemetryData(data); + await sendMetric('capacitor_cli_command', data); if (error) { throw error; @@ -108,6 +92,22 @@ export function telemetryAction( }; } +export async function sendMetric(name: string, data: D): Promise { + const sysconfig = await readConfig(); + + if (sysconfig.telemetry && isInteractive()) { + const message: Metric = { + name, + timestamp: new Date().toISOString(), + session_id: sysconfig.machine, + source: 'capacitor_cli', + value: data, + }; + + await send({ type: 'telemetry', data: message }); + } +} + /** * Walk through the command's parent tree and construct a space-separated name. * From 75c6b1d2d8708640861f44d715f9485ee5eedd19 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 6 Jan 2021 11:36:41 -0800 Subject: [PATCH 4/7] app identifier --- cli/src/telemetry.ts | 32 +++++++++++++++++++++++++++++++- cli/src/util/subprocess.ts | 3 ++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index b5ec791353..4502c38814 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -1,11 +1,15 @@ import { Command } from 'commander'; +import Debug from 'debug'; import c from './colors'; import type { Config } from './definitions'; import { send } from './ipc'; import { readConfig } from './sysconfig'; +import { getCommandOutput } from './util/subprocess'; import { isInteractive } from './util/term'; +const debug = Debug('capacitor:telemetry'); + export const THANK_YOU = `\nThank you for helping to make Capacitor better! 💖` + `\nInformation about the data we collect is available on our website: ${c.strong( @@ -73,7 +77,7 @@ export function telemetryAction( ]); const data: CommandMetricData = { - app_id: '', // TODO + app_id: await getAppIdentifier(config), command, arguments: cmd.args.join(' '), options: JSON.stringify(cmd.opts()), @@ -108,6 +112,32 @@ export async function sendMetric(name: string, data: D): Promise { } } +/** + * Get a unique anonymous identifier for this app. + */ +async function getAppIdentifier(config: Config): Promise { + const { createHash } = await import('crypto'); + + // get the first commit hash, which should be universally unique + const output = await getCommandOutput( + 'git', + ['rev-list', '--max-parents=0', 'HEAD'], + { cwd: config.app.rootDir }, + ); + + const firstLine = output?.split('\n')[0]; + + if (!firstLine) { + debug('Could not obtain unique app identifier'); + return null; + } + + // use sha1 to create a one-way hash to anonymize + const id = createHash('sha1').update(firstLine).digest('hex'); + + return id; +} + /** * Walk through the command's parent tree and construct a space-separated name. * diff --git a/cli/src/util/subprocess.ts b/cli/src/util/subprocess.ts index 27c5ecd022..6db42009d7 100644 --- a/cli/src/util/subprocess.ts +++ b/cli/src/util/subprocess.ts @@ -26,9 +26,10 @@ export async function runCommand( export async function getCommandOutput( command: string, args: readonly string[], + options: RunCommandOptions = {}, ): Promise { try { - return (await runCommand(command, args)).trim(); + return (await runCommand(command, args, options)).trim(); } catch (e) { return null; } From 6609336de3b9bee88b0912e812f65f3b35b6a940 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 6 Jan 2021 13:20:53 -0800 Subject: [PATCH 5/7] prompt for telemetry --- cli/src/sysconfig.ts | 5 ++-- cli/src/telemetry.ts | 57 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/cli/src/sysconfig.ts b/cli/src/sysconfig.ts index 20200bdaa8..4b3f7f3ed4 100644 --- a/cli/src/sysconfig.ts +++ b/cli/src/sysconfig.ts @@ -18,8 +18,10 @@ export interface SystemConfig { /** * Whether telemetry is enabled or not. + * + * If undefined, a choice has not yet been made. */ - readonly telemetry: boolean; + readonly telemetry?: boolean; } export async function readConfig(): Promise { @@ -34,7 +36,6 @@ export async function readConfig(): Promise { const sysconfig: SystemConfig = { machine: uuidv4(), - telemetry: true, }; await writeConfig(sysconfig); diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index 4502c38814..c52c950589 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -4,7 +4,9 @@ import Debug from 'debug'; import c from './colors'; import type { Config } from './definitions'; import { send } from './ipc'; -import { readConfig } from './sysconfig'; +import { logPrompt, output } from './log'; +import type { SystemConfig } from './sysconfig'; +import { readConfig, writeConfig } from './sysconfig'; import { getCommandOutput } from './util/subprocess'; import { isInteractive } from './util/term'; @@ -88,7 +90,18 @@ export function telemetryAction( ...Object.fromEntries(versions), }; - await sendMetric('capacitor_cli_command', data); + if (isInteractive()) { + let sysconfig = await readConfig(); + + if (typeof sysconfig.telemetry === 'undefined') { + const confirm = await promptForTelemetry(); + sysconfig = { ...sysconfig, telemetry: confirm }; + + await writeConfig(sysconfig); + } + + await sendMetric(sysconfig, 'capacitor_cli_command', data); + } if (error) { throw error; @@ -96,9 +109,14 @@ export function telemetryAction( }; } -export async function sendMetric(name: string, data: D): Promise { - const sysconfig = await readConfig(); - +/** + * If telemetry is enabled, send a metric via IPC to a forked process for uploading. + */ +export async function sendMetric( + sysconfig: Pick, + name: string, + data: D, +): Promise { if (sysconfig.telemetry && isInteractive()) { const message: Metric = { name, @@ -109,9 +127,38 @@ export async function sendMetric(name: string, data: D): Promise { }; await send({ type: 'telemetry', data: message }); + } else { + debug( + 'Telemetry is off (user choice, non-interactive terminal, or CI)--not sending metric', + ); } } +async function promptForTelemetry(): Promise { + const { confirm } = await logPrompt( + `${c.strong( + 'Would you like to help improve Capacitor by sharing anonymous usage data? 💖', + )}\n` + + `Read more about what is being collected and why here: ${c.strong( + 'https://capacitorjs.com/telemetry', + )}. You can change your mind at any time by using the ${c.input( + 'npx cap telemetry', + )} command.`, + { + type: 'confirm', + name: 'confirm', + message: 'Share anonymous usage data?', + initial: true, + }, + ); + + if (confirm) { + output.write(THANK_YOU); + } + + return confirm; +} + /** * Get a unique anonymous identifier for this app. */ From b5c0ce3879422b347af2646409c606c4dd158323 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Wed, 6 Jan 2021 15:22:41 -0800 Subject: [PATCH 6/7] couple small things --- cli/src/tasks/init.ts | 10 +++++----- cli/src/telemetry.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli/src/tasks/init.ts b/cli/src/tasks/init.ts index 2fd4979fa2..989f8295cf 100644 --- a/cli/src/tasks/init.ts +++ b/cli/src/tasks/init.ts @@ -58,12 +58,12 @@ export async function initCommand( printNextSteps(config); } catch (e) { - output.write( - 'Usage: npx cap init appName appId\n' + - 'Example: npx cap init "My App" "com.example.myapp"\n\n', - ); - if (!isFatal(e)) { + output.write( + 'Usage: npx cap init appName appId\n' + + 'Example: npx cap init "My App" "com.example.myapp"\n\n', + ); + fatal(e.stack ?? e); } diff --git a/cli/src/telemetry.ts b/cli/src/telemetry.ts index c52c950589..cef34c4a57 100644 --- a/cli/src/telemetry.ts +++ b/cli/src/telemetry.ts @@ -93,7 +93,7 @@ export function telemetryAction( if (isInteractive()) { let sysconfig = await readConfig(); - if (typeof sysconfig.telemetry === 'undefined') { + if (!error && typeof sysconfig.telemetry === 'undefined') { const confirm = await promptForTelemetry(); sysconfig = { ...sysconfig, telemetry: confirm }; From a95521498881fa174022aa7d8c677efe166e1604 Mon Sep 17 00:00:00 2001 From: Daniel Imhoff Date: Thu, 7 Jan 2021 15:07:12 -0800 Subject: [PATCH 7/7] switch off staging --- cli/src/ipc.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/ipc.ts b/cli/src/ipc.ts index fc62f8801e..2344ff054e 100644 --- a/cli/src/ipc.ts +++ b/cli/src/ipc.ts @@ -53,7 +53,7 @@ export async function receive(msg: IPCMessage): Promise { // This request is only made if telemetry is on. const req = request( { - hostname: 'api-staging.ionicjs.com', // TODO + hostname: 'api.ionicjs.com', port: 443, path: '/events/metrics', method: 'POST',