Skip to content

Commit

Permalink
feat(cli): add telemetry and cli features (#2964)
Browse files Browse the repository at this point in the history
* users may opt-out at any time
* provide a CLI interface for checking current telemetry status, toggling participation
* measure tasks that the Stencil CLI performs today, anonymize data, and send to Ionic for aggregation
  • Loading branch information
splitinfinities authored Aug 4, 2021
1 parent bdd9d6f commit 1381cc7
Show file tree
Hide file tree
Showing 17 changed files with 743 additions and 43 deletions.
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 16 additions & 6 deletions src/cli/ionic-config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getCompilerSystem } from './state/stencil-cli-config';
import { readJson, uuidv4 } from './telemetry/helpers';
import { readJson, uuidv4, UUID_REGEX } from './telemetry/helpers';

export const isTest = () => process.env.JEST_WORKER_ID !== undefined

export const defaultConfig = () =>
getCompilerSystem().resolvePath(`${getCompilerSystem().homeDir()}/.ionic/config.json`);
getCompilerSystem().resolvePath(`${getCompilerSystem().homeDir()}/.ionic/${isTest() ? "tmp-config.json" : "config.json"}`);

export const defaultConfigDirectory = () => getCompilerSystem().resolvePath(`${getCompilerSystem().homeDir()}/.ionic`);

Expand All @@ -21,21 +23,29 @@ export async function readConfig(): Promise<TelemetryConfig> {
};

await writeConfig(config);
} else if (!!config && !config['tokens.telemetry'].match(UUID_REGEX)) {
const newUuid = uuidv4();
await writeConfig({...config, 'tokens.telemetry': newUuid });
config['tokens.telemetry'] = newUuid;
}

return config;
}

export async function writeConfig(config: TelemetryConfig): Promise<void> {
export async function writeConfig(config: TelemetryConfig): Promise<boolean> {
let result = false;
try {
await getCompilerSystem().createDir(defaultConfigDirectory(), { recursive: true });
await getCompilerSystem().writeFile(defaultConfig(), JSON.stringify(config));
await getCompilerSystem().writeFile(defaultConfig(), JSON.stringify(config, null, 2));
result = true;
} catch (error) {
console.error(`Stencil Telemetry: couldn't write configuration file to ${defaultConfig()} - ${error}.`);
}

return result;
}

export async function updateConfig(newOptions: TelemetryConfig): Promise<void> {
export async function updateConfig(newOptions: TelemetryConfig): Promise<boolean> {
const config = await readConfig();
await writeConfig(Object.assign(config, newOptions));
return await writeConfig(Object.assign(config, newOptions));
}
31 changes: 22 additions & 9 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,19 @@ import { taskPrerender } from './task-prerender';
import { taskServe } from './task-serve';
import { taskTest } from './task-test';
import { initializeStencilCLIConfig } from './state/stencil-cli-config';
import { taskTelemetry } from './task-telemetry';
import { telemetryAction } from './telemetry/telemetry';

export const run = async (init: CliInitOptions) => {
const { args, logger, sys } = init;

// Initialize the singleton so we can use this throughout the lifecycle of the CLI.
const stencilCLIConfig = initializeStencilCLIConfig({ args, logger, sys});
const stencilCLIConfig = initializeStencilCLIConfig({ args, logger, sys });

try {
const flags = parseFlags(args, sys);
const task = flags.task;

if (flags.debug || flags.verbose) {
logger.setLevel('debug');
}
Expand All @@ -43,7 +46,7 @@ export const run = async (init: CliInitOptions) => {
stencilCLIConfig.flags = flags;

if (task === 'help' || flags.help) {
taskHelp(sys, logger);
taskHelp();
return;
}

Expand Down Expand Up @@ -79,7 +82,9 @@ export const run = async (init: CliInitOptions) => {
loadedCompilerLog(sys, logger, flags, coreCompiler);

if (task === 'info') {
taskInfo(coreCompiler, sys, logger);
await telemetryAction(async () => {
await taskInfo(coreCompiler, sys, logger);
});
return;
}

Expand All @@ -99,13 +104,17 @@ export const run = async (init: CliInitOptions) => {
}
}

stencilCLIConfig.validatedConfig = validated;

if (isFunction(sys.applyGlobalPatch)) {
sys.applyGlobalPatch(validated.config.rootDir);
}

await sys.ensureResources({ rootDir: validated.config.rootDir, logger, dependencies: dependencies as any });

await runTask(coreCompiler, validated.config, task);
await telemetryAction(async () => {
await runTask(coreCompiler, validated.config, task);
});
} catch (e) {
if (!shouldIgnoreError(e)) {
logger.error(`uncaught cli error: ${e}${logger.getLevel() === 'debug' ? e.stack : ''}`);
Expand All @@ -127,15 +136,15 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
await taskDocs(coreCompiler, config);
break;

case 'help':
taskHelp(config.sys, config.logger);
break;

case 'generate':
case 'g':
await taskGenerate(coreCompiler, config);
break;

case 'help':
taskHelp();
break;

case 'prerender':
await taskPrerender(coreCompiler, config);
break;
Expand All @@ -144,6 +153,10 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
await taskServe(config);
break;

case 'telemetry':
await taskTelemetry();
break;

case 'test':
await taskTest(config);
break;
Expand All @@ -154,7 +167,7 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:

default:
config.logger.error(`${config.logger.emoji('❌ ')}Invalid stencil command, please see the options below:`);
taskHelp(config.sys, config.logger);
taskHelp();
return config.sys.exit(1);
}
};
17 changes: 16 additions & 1 deletion src/cli/state/stencil-cli-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Logger, CompilerSystem, ConfigFlags } from '../../declarations';
import type { Logger, CompilerSystem, ConfigFlags, LoadConfigResults } from '../../declarations';
export type CoreCompiler = typeof import('@stencil/core/compiler');

export interface StencilCLIConfigArgs {
Expand All @@ -8,6 +8,7 @@ export interface StencilCLIConfigArgs {
sys: CompilerSystem;
flags?: ConfigFlags;
coreCompiler?: CoreCompiler;
validatedConfig?: LoadConfigResults;
}

export default class StencilCLIConfig {
Expand All @@ -19,11 +20,14 @@ export default class StencilCLIConfig {
private _flags: ConfigFlags | undefined;
private _task: string | undefined;
private _coreCompiler: CoreCompiler | undefined;
private _validatedConfig: LoadConfigResults | undefined;

private constructor(options: StencilCLIConfigArgs) {
this._args = options?.args || [];
this._logger = options?.logger;
this._sys = options?.sys;
this._flags = options?.flags || undefined;
this._validatedConfig = options?.validatedConfig || undefined;
}

public static getInstance(options?: StencilCLIConfigArgs): StencilCLIConfig {
Expand All @@ -34,6 +38,10 @@ export default class StencilCLIConfig {
return StencilCLIConfig.instance;
}

public resetInstance() {
delete StencilCLIConfig.instance;
}

public get logger() {
return this._logger;
}
Expand Down Expand Up @@ -75,6 +83,13 @@ export default class StencilCLIConfig {
public set coreCompiler(coreCompiler: CoreCompiler) {
this._coreCompiler = coreCompiler;
}

public get validatedConfig() {
return this._validatedConfig;
}
public set validatedConfig(validatedConfig: LoadConfigResults) {
this._validatedConfig = validatedConfig;
}
}

export function initializeStencilCLIConfig(options: StencilCLIConfigArgs): StencilCLIConfig {
Expand Down
3 changes: 3 additions & 0 deletions src/cli/task-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { runPrerenderTask } from './task-prerender';
import { startCheckVersion, printCheckVersionResults } from './check-version';
import { startupCompilerLog } from './logs';
import { taskWatch } from './task-watch';
import { telemetryBuildFinishedAction } from './telemetry/telemetry';

export const taskBuild = async (coreCompiler: CoreCompiler, config: Config) => {
if (config.flags.watch) {
Expand All @@ -23,6 +24,8 @@ export const taskBuild = async (coreCompiler: CoreCompiler, config: Config) => {
const compiler = await coreCompiler.createCompiler(config);
const results = await compiler.build();

await telemetryBuildFinishedAction(results);

await compiler.destroy();

if (results.hasError) {
Expand Down
33 changes: 21 additions & 12 deletions src/cli/task-help.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { CompilerSystem, Logger } from '../declarations';
import { getCompilerSystem, getLogger } from './state/stencil-cli-config';
import { taskTelemetry } from './task-telemetry';

export const taskHelp = (sys: CompilerSystem, logger: Logger) => {
const p = logger.dim(sys.details.platform === 'windows' ? '>' : '$');
export const taskHelp = async () => {
const logger = getLogger();
const sys = getCompilerSystem();

const prompt = logger.dim(sys.details.platform === 'windows' ? '>' : '$');

console.log(`
${logger.bold('Build:')} ${logger.dim('Build components for development or production.')}
${p} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')}
${prompt} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')}
${logger.cyan('--dev')} ${logger.dim('.............')} Development build
${logger.cyan('--watch')} ${logger.dim('...........')} Rebuild when files update
Expand All @@ -21,24 +25,29 @@ export const taskHelp = (sys: CompilerSystem, logger: Logger) => {
${logger.bold('Test:')} ${logger.dim('Run unit and end-to-end tests.')}
${p} ${logger.green('stencil test [--spec] [--e2e]')}
${prompt} ${logger.green('stencil test [--spec] [--e2e]')}
${logger.cyan('--spec')} ${logger.dim('............')} Run unit tests with Jest
${logger.cyan('--e2e')} ${logger.dim('.............')} Run e2e tests with Puppeteer
${logger.bold('Generate:')} ${logger.dim('Bootstrap components.')}
${p} ${logger.green('stencil generate')} or ${logger.green('stencil g')}
${prompt} ${logger.green('stencil generate')} or ${logger.green('stencil g')}
`);

${logger.bold('Examples:')}
await taskTelemetry();

${p} ${logger.green('stencil build --dev --watch --serve')}
${p} ${logger.green('stencil build --prerender')}
${p} ${logger.green('stencil test --spec --e2e')}
${p} ${logger.green('stencil generate')}
${p} ${logger.green('stencil g my-component')}
console.log(`
${logger.bold('Examples:')}
${prompt} ${logger.green('stencil build --dev --watch --serve')}
${prompt} ${logger.green('stencil build --prerender')}
${prompt} ${logger.green('stencil test --spec --e2e')}
${prompt} ${logger.green('stencil telemetry on')}
${prompt} ${logger.green('stencil generate')}
${prompt} ${logger.green('stencil g my-component')}
`);
};
42 changes: 42 additions & 0 deletions src/cli/task-telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getCompilerSystem, getLogger, getStencilCLIConfig } from './state/stencil-cli-config';
import { checkTelemetry, disableTelemetry, enableTelemetry } from './telemetry/telemetry';

export const taskTelemetry = async () => {
const logger = getLogger();
const prompt = logger.dim(getCompilerSystem().details.platform === 'windows' ? '>' : '$');
const isEnabling = getStencilCLIConfig().flags.args.includes('on');
const isDisabling = getStencilCLIConfig().flags.args.includes('off');
const INFORMATION = `Opt in or our of telemetry. Information about the data we collect is available on our website: ${logger.bold(
'https://stenciljs.com/telemetry',
)}`;
const THANK_YOU = `Thank you for helping to make Stencil better! 💖`;
const ENABLED_MESSAGE = `${logger.green('Enabled')}. ${THANK_YOU}\n\n`;
const DISABLED_MESSAGE = `${logger.red('Disabled')}\n\n`;
const hasTelemetry = await checkTelemetry();

if (isEnabling) {
const result = await enableTelemetry();
result
? console.log(`\n ${logger.bold('Telemetry is now ') + ENABLED_MESSAGE}`)
: console.log(`Something went wrong when enabling Telemetry.`);
return;
}

if (isDisabling) {
const result = await disableTelemetry();
result
? console.log(`\n ${logger.bold('Telemetry is now ') + DISABLED_MESSAGE}`)
: console.log(`Something went wrong when disabling Telemetry.`);
return;
}

console.log(` ${logger.bold('Telemetry:')} ${logger.dim(INFORMATION)}`);

console.log(`\n ${logger.bold('Status')}: ${hasTelemetry ? ENABLED_MESSAGE : DISABLED_MESSAGE}`);

console.log(` ${prompt} ${logger.green('stencil telemetry [off|on]')}
${logger.cyan('off')} ${logger.dim('.............')} Disable sharing anonymous usage data
${logger.cyan('on')} ${logger.dim('..............')} Enable sharing anonymous usage data
`);
};
10 changes: 10 additions & 0 deletions src/cli/telemetry/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export const isInteractive = (object?: TerminalInfo): boolean => {
return terminalInfo.tty && !terminalInfo.ci;
};

export const UUID_REGEX = new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i);

// Plucked from https://github.com/ionic-team/capacitor/blob/b893a57aaaf3a16e13db9c33037a12f1a5ac92e0/cli/src/util/uuid.ts
export function uuidv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
Expand All @@ -51,3 +53,11 @@ export async function readJson(path: string) {
const file = await getCompilerSystem().readFile(path);
return !!file && JSON.parse(file);
}

export function hasDebug() {
return getStencilCLIConfig().flags.debug;
}

export function hasVerbose() {
return getStencilCLIConfig().flags.verbose && hasDebug();
}
11 changes: 11 additions & 0 deletions src/cli/telemetry/shouldTrack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { isInteractive } from './helpers';
import { checkTelemetry } from './telemetry';

/**
* Used to determine if tracking should occur.
* @param ci whether or not the process is running in a Continuous Integration (CI) environment
* @returns true if telemetry should be sent, false otherwise
*/
export async function shouldTrack(ci?: boolean) {
return !ci && isInteractive() && (await checkTelemetry());
}
Loading

0 comments on commit 1381cc7

Please sign in to comment.