Skip to content

Commit 1381cc7

Browse files
feat(cli): add telemetry and cli features (#2964)
* 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
1 parent bdd9d6f commit 1381cc7

17 files changed

+743
-43
lines changed

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli/ionic-config.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { getCompilerSystem } from './state/stencil-cli-config';
2-
import { readJson, uuidv4 } from './telemetry/helpers';
2+
import { readJson, uuidv4, UUID_REGEX } from './telemetry/helpers';
3+
4+
export const isTest = () => process.env.JEST_WORKER_ID !== undefined
35

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

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

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

2325
await writeConfig(config);
26+
} else if (!!config && !config['tokens.telemetry'].match(UUID_REGEX)) {
27+
const newUuid = uuidv4();
28+
await writeConfig({...config, 'tokens.telemetry': newUuid });
29+
config['tokens.telemetry'] = newUuid;
2430
}
2531

2632
return config;
2733
}
2834

29-
export async function writeConfig(config: TelemetryConfig): Promise<void> {
35+
export async function writeConfig(config: TelemetryConfig): Promise<boolean> {
36+
let result = false;
3037
try {
3138
await getCompilerSystem().createDir(defaultConfigDirectory(), { recursive: true });
32-
await getCompilerSystem().writeFile(defaultConfig(), JSON.stringify(config));
39+
await getCompilerSystem().writeFile(defaultConfig(), JSON.stringify(config, null, 2));
40+
result = true;
3341
} catch (error) {
3442
console.error(`Stencil Telemetry: couldn't write configuration file to ${defaultConfig()} - ${error}.`);
3543
}
44+
45+
return result;
3646
}
3747

38-
export async function updateConfig(newOptions: TelemetryConfig): Promise<void> {
48+
export async function updateConfig(newOptions: TelemetryConfig): Promise<boolean> {
3949
const config = await readConfig();
40-
await writeConfig(Object.assign(config, newOptions));
50+
return await writeConfig(Object.assign(config, newOptions));
4151
}

src/cli/run.ts

+22-9
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,19 @@ import { taskPrerender } from './task-prerender';
1414
import { taskServe } from './task-serve';
1515
import { taskTest } from './task-test';
1616
import { initializeStencilCLIConfig } from './state/stencil-cli-config';
17+
import { taskTelemetry } from './task-telemetry';
18+
import { telemetryAction } from './telemetry/telemetry';
1719

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

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

2426
try {
2527
const flags = parseFlags(args, sys);
2628
const task = flags.task;
29+
2730
if (flags.debug || flags.verbose) {
2831
logger.setLevel('debug');
2932
}
@@ -43,7 +46,7 @@ export const run = async (init: CliInitOptions) => {
4346
stencilCLIConfig.flags = flags;
4447

4548
if (task === 'help' || flags.help) {
46-
taskHelp(sys, logger);
49+
taskHelp();
4750
return;
4851
}
4952

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

8184
if (task === 'info') {
82-
taskInfo(coreCompiler, sys, logger);
85+
await telemetryAction(async () => {
86+
await taskInfo(coreCompiler, sys, logger);
87+
});
8388
return;
8489
}
8590

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

107+
stencilCLIConfig.validatedConfig = validated;
108+
102109
if (isFunction(sys.applyGlobalPatch)) {
103110
sys.applyGlobalPatch(validated.config.rootDir);
104111
}
105112

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

108-
await runTask(coreCompiler, validated.config, task);
115+
await telemetryAction(async () => {
116+
await runTask(coreCompiler, validated.config, task);
117+
});
109118
} catch (e) {
110119
if (!shouldIgnoreError(e)) {
111120
logger.error(`uncaught cli error: ${e}${logger.getLevel() === 'debug' ? e.stack : ''}`);
@@ -127,15 +136,15 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
127136
await taskDocs(coreCompiler, config);
128137
break;
129138

130-
case 'help':
131-
taskHelp(config.sys, config.logger);
132-
break;
133-
134139
case 'generate':
135140
case 'g':
136141
await taskGenerate(coreCompiler, config);
137142
break;
138143

144+
case 'help':
145+
taskHelp();
146+
break;
147+
139148
case 'prerender':
140149
await taskPrerender(coreCompiler, config);
141150
break;
@@ -144,6 +153,10 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
144153
await taskServe(config);
145154
break;
146155

156+
case 'telemetry':
157+
await taskTelemetry();
158+
break;
159+
147160
case 'test':
148161
await taskTest(config);
149162
break;
@@ -154,7 +167,7 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
154167

155168
default:
156169
config.logger.error(`${config.logger.emoji('❌ ')}Invalid stencil command, please see the options below:`);
157-
taskHelp(config.sys, config.logger);
170+
taskHelp();
158171
return config.sys.exit(1);
159172
}
160173
};

src/cli/state/stencil-cli-config.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Logger, CompilerSystem, ConfigFlags } from '../../declarations';
1+
import type { Logger, CompilerSystem, ConfigFlags, LoadConfigResults } from '../../declarations';
22
export type CoreCompiler = typeof import('@stencil/core/compiler');
33

44
export interface StencilCLIConfigArgs {
@@ -8,6 +8,7 @@ export interface StencilCLIConfigArgs {
88
sys: CompilerSystem;
99
flags?: ConfigFlags;
1010
coreCompiler?: CoreCompiler;
11+
validatedConfig?: LoadConfigResults;
1112
}
1213

1314
export default class StencilCLIConfig {
@@ -19,11 +20,14 @@ export default class StencilCLIConfig {
1920
private _flags: ConfigFlags | undefined;
2021
private _task: string | undefined;
2122
private _coreCompiler: CoreCompiler | undefined;
23+
private _validatedConfig: LoadConfigResults | undefined;
2224

2325
private constructor(options: StencilCLIConfigArgs) {
2426
this._args = options?.args || [];
2527
this._logger = options?.logger;
2628
this._sys = options?.sys;
29+
this._flags = options?.flags || undefined;
30+
this._validatedConfig = options?.validatedConfig || undefined;
2731
}
2832

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

41+
public resetInstance() {
42+
delete StencilCLIConfig.instance;
43+
}
44+
3745
public get logger() {
3846
return this._logger;
3947
}
@@ -75,6 +83,13 @@ export default class StencilCLIConfig {
7583
public set coreCompiler(coreCompiler: CoreCompiler) {
7684
this._coreCompiler = coreCompiler;
7785
}
86+
87+
public get validatedConfig() {
88+
return this._validatedConfig;
89+
}
90+
public set validatedConfig(validatedConfig: LoadConfigResults) {
91+
this._validatedConfig = validatedConfig;
92+
}
7893
}
7994

8095
export function initializeStencilCLIConfig(options: StencilCLIConfigArgs): StencilCLIConfig {

src/cli/task-build.ts

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { runPrerenderTask } from './task-prerender';
44
import { startCheckVersion, printCheckVersionResults } from './check-version';
55
import { startupCompilerLog } from './logs';
66
import { taskWatch } from './task-watch';
7+
import { telemetryBuildFinishedAction } from './telemetry/telemetry';
78

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

27+
await telemetryBuildFinishedAction(results);
28+
2629
await compiler.destroy();
2730

2831
if (results.hasError) {

src/cli/task-help.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import type { CompilerSystem, Logger } from '../declarations';
1+
import { getCompilerSystem, getLogger } from './state/stencil-cli-config';
2+
import { taskTelemetry } from './task-telemetry';
23

3-
export const taskHelp = (sys: CompilerSystem, logger: Logger) => {
4-
const p = logger.dim(sys.details.platform === 'windows' ? '>' : '$');
4+
export const taskHelp = async () => {
5+
const logger = getLogger();
6+
const sys = getCompilerSystem();
7+
8+
const prompt = logger.dim(sys.details.platform === 'windows' ? '>' : '$');
59

610
console.log(`
711
${logger.bold('Build:')} ${logger.dim('Build components for development or production.')}
812
9-
${p} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')}
13+
${prompt} ${logger.green('stencil build [--dev] [--watch] [--prerender] [--debug]')}
1014
1115
${logger.cyan('--dev')} ${logger.dim('.............')} Development build
1216
${logger.cyan('--watch')} ${logger.dim('...........')} Rebuild when files update
@@ -21,24 +25,29 @@ export const taskHelp = (sys: CompilerSystem, logger: Logger) => {
2125
2226
${logger.bold('Test:')} ${logger.dim('Run unit and end-to-end tests.')}
2327
24-
${p} ${logger.green('stencil test [--spec] [--e2e]')}
28+
${prompt} ${logger.green('stencil test [--spec] [--e2e]')}
2529
2630
${logger.cyan('--spec')} ${logger.dim('............')} Run unit tests with Jest
2731
${logger.cyan('--e2e')} ${logger.dim('.............')} Run e2e tests with Puppeteer
2832
2933
3034
${logger.bold('Generate:')} ${logger.dim('Bootstrap components.')}
3135
32-
${p} ${logger.green('stencil generate')} or ${logger.green('stencil g')}
36+
${prompt} ${logger.green('stencil generate')} or ${logger.green('stencil g')}
3337
38+
`);
3439

35-
${logger.bold('Examples:')}
40+
await taskTelemetry();
3641

37-
${p} ${logger.green('stencil build --dev --watch --serve')}
38-
${p} ${logger.green('stencil build --prerender')}
39-
${p} ${logger.green('stencil test --spec --e2e')}
40-
${p} ${logger.green('stencil generate')}
41-
${p} ${logger.green('stencil g my-component')}
42+
console.log(`
43+
${logger.bold('Examples:')}
4244
45+
46+
${prompt} ${logger.green('stencil build --dev --watch --serve')}
47+
${prompt} ${logger.green('stencil build --prerender')}
48+
${prompt} ${logger.green('stencil test --spec --e2e')}
49+
${prompt} ${logger.green('stencil telemetry on')}
50+
${prompt} ${logger.green('stencil generate')}
51+
${prompt} ${logger.green('stencil g my-component')}
4352
`);
4453
};

src/cli/task-telemetry.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { getCompilerSystem, getLogger, getStencilCLIConfig } from './state/stencil-cli-config';
2+
import { checkTelemetry, disableTelemetry, enableTelemetry } from './telemetry/telemetry';
3+
4+
export const taskTelemetry = async () => {
5+
const logger = getLogger();
6+
const prompt = logger.dim(getCompilerSystem().details.platform === 'windows' ? '>' : '$');
7+
const isEnabling = getStencilCLIConfig().flags.args.includes('on');
8+
const isDisabling = getStencilCLIConfig().flags.args.includes('off');
9+
const INFORMATION = `Opt in or our of telemetry. Information about the data we collect is available on our website: ${logger.bold(
10+
'https://stenciljs.com/telemetry',
11+
)}`;
12+
const THANK_YOU = `Thank you for helping to make Stencil better! 💖`;
13+
const ENABLED_MESSAGE = `${logger.green('Enabled')}. ${THANK_YOU}\n\n`;
14+
const DISABLED_MESSAGE = `${logger.red('Disabled')}\n\n`;
15+
const hasTelemetry = await checkTelemetry();
16+
17+
if (isEnabling) {
18+
const result = await enableTelemetry();
19+
result
20+
? console.log(`\n ${logger.bold('Telemetry is now ') + ENABLED_MESSAGE}`)
21+
: console.log(`Something went wrong when enabling Telemetry.`);
22+
return;
23+
}
24+
25+
if (isDisabling) {
26+
const result = await disableTelemetry();
27+
result
28+
? console.log(`\n ${logger.bold('Telemetry is now ') + DISABLED_MESSAGE}`)
29+
: console.log(`Something went wrong when disabling Telemetry.`);
30+
return;
31+
}
32+
33+
console.log(` ${logger.bold('Telemetry:')} ${logger.dim(INFORMATION)}`);
34+
35+
console.log(`\n ${logger.bold('Status')}: ${hasTelemetry ? ENABLED_MESSAGE : DISABLED_MESSAGE}`);
36+
37+
console.log(` ${prompt} ${logger.green('stencil telemetry [off|on]')}
38+
39+
${logger.cyan('off')} ${logger.dim('.............')} Disable sharing anonymous usage data
40+
${logger.cyan('on')} ${logger.dim('..............')} Enable sharing anonymous usage data
41+
`);
42+
};

src/cli/telemetry/helpers.ts

+10
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const isInteractive = (object?: TerminalInfo): boolean => {
3737
return terminalInfo.tty && !terminalInfo.ci;
3838
};
3939

40+
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);
41+
4042
// Plucked from https://github.com/ionic-team/capacitor/blob/b893a57aaaf3a16e13db9c33037a12f1a5ac92e0/cli/src/util/uuid.ts
4143
export function uuidv4(): string {
4244
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
@@ -51,3 +53,11 @@ export async function readJson(path: string) {
5153
const file = await getCompilerSystem().readFile(path);
5254
return !!file && JSON.parse(file);
5355
}
56+
57+
export function hasDebug() {
58+
return getStencilCLIConfig().flags.debug;
59+
}
60+
61+
export function hasVerbose() {
62+
return getStencilCLIConfig().flags.verbose && hasDebug();
63+
}

src/cli/telemetry/shouldTrack.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { isInteractive } from './helpers';
2+
import { checkTelemetry } from './telemetry';
3+
4+
/**
5+
* Used to determine if tracking should occur.
6+
* @param ci whether or not the process is running in a Continuous Integration (CI) environment
7+
* @returns true if telemetry should be sent, false otherwise
8+
*/
9+
export async function shouldTrack(ci?: boolean) {
10+
return !ci && isInteractive() && (await checkTelemetry());
11+
}

0 commit comments

Comments
 (0)