Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): [STENCIL-12] Adding Telemetry and CLI features. #2964

Merged
merged 24 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f1deae7
feat(cli): [STENCIL-12] Adding Telemetry and CLI for users to toggle …
splitinfinities Jul 26, 2021
628c0c7
Fixes from Ryan
splitinfinities Jul 26, 2021
d52a7d1
Fixes from Ryan + better interop by using fetch instead of request.
splitinfinities Jul 27, 2021
f524372
Update src/cli/task-telemetry.ts
splitinfinities Jul 27, 2021
d889830
Add missed new line in telemetry
splitinfinities Jul 27, 2021
c635329
Update src/cli/telemetry/shouldTrack.ts
splitinfinities Jul 27, 2021
739e62c
Update src/cli/telemetry/telemetry.ts
splitinfinities Jul 27, 2021
cdf1879
Update src/cli/telemetry/telemetry.ts
splitinfinities Jul 27, 2021
dbbb0cb
Feedback from Ryan
splitinfinities Jul 27, 2021
aa5bb85
Feedback from Lars + Ryan
splitinfinities Jul 28, 2021
5ca6854
Adding guard clause on fetch in node-sys
splitinfinities Jul 28, 2021
3d466ed
Update src/cli/telemetry/telemetry.ts
splitinfinities Jul 28, 2021
871ac40
Update src/cli/telemetry/telemetry.ts
splitinfinities Jul 28, 2021
60865ea
Fix ts issues and revert the global pointer for fetch because it may …
splitinfinities Jul 28, 2021
bce8319
Update src/cli/telemetry/telemetry.ts
splitinfinities Jul 28, 2021
6448a29
Feedback from Lars
splitinfinities Jul 28, 2021
250f28c
Add session to the Metric as opposed to the data sent
splitinfinities Jul 28, 2021
66c8019
Update src/cli/ionic-config.ts to match Ionic's CLI
splitinfinities Jul 30, 2021
c251962
Feedback from Lars, adding the pwa prop instead of overloading the ta…
splitinfinities Jul 30, 2021
45f5a11
Add validation on reading the telemetry token
splitinfinities Jul 30, 2021
2e01743
Write tests to a temporary file.
splitinfinities Aug 2, 2021
b8986c7
Improve testability of Telemetry code
splitinfinities Aug 2, 2021
f9644b8
Fix from Ryan
splitinfinities Aug 3, 2021
063cc1c
Fixes from Ryan
splitinfinities Aug 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

10 changes: 7 additions & 3 deletions src/cli/ionic-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@ export async function readConfig(): Promise<TelemetryConfig> {
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));
splitinfinities marked this conversation as resolved.
Show resolved Hide resolved
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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add tests on the return value now that we're returning a primitive? We should be able to assert that we return true when writeConfig is successful, and false when it fails

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, great point! Yes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@splitinfinities did we get this under test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine ATM - normally I'd like us to get a little more granular there with respect to what we're testing (calling the ionic-config exposed functions directly) and splitting the tests out based on the assertion, but I think we can leave that for a future day

}
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) {
ltm marked this conversation as resolved.
Show resolved Hide resolved
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);
}
};
13 changes: 12 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 Down Expand Up @@ -75,6 +79,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')}
rwaskiewicz marked this conversation as resolved.
Show resolved Hide resolved
${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');
Comment on lines +5 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment above, I'm not sure what the appropriate interface to use is here - should the logger be pulled from getStencilCLIConfig?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getLogger() does pull from the getStencilCLIConfig() singleton - it's a helper method to return that similar to getCompilerSystem(). I may also be misreading your question. Are you asking if logger should be stored on the singleton, as in philosophically?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it seems like there's two ways to get a a logger instance:

const loggerViaMethod = getLogger();
const loggerViaSingletonAccessor = getStencilCLIConfig().logger;

As a user of the singleton, it's not exactly clear to me if there's a preferred way to get the logger without reading the code that the former is a wrapper around the latter. The ergonomics of pulling data off the singleton seems a little inconsistent to me - for some fields I need to call getStencilCLIConfig(), others have a helper function.

That said, I'm not sure adding a helper function for each private method necessarily helps here. It gives us consistency/parity where there's a helper function for each private field that we expose via an accessor, but it doesn't answer the "which should I use?" question.

That's all a long winded way of saying "I think this is fine as is for now, because I don't have a great solution to offer up ATM". It's something we should be cognizant of as we evolve this class, but no action required ATM

Copy link
Contributor Author

@splitinfinities splitinfinities Jul 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally you'll see me throughout this PR use the helper functions, until I need to update the value, at which point I'll use:

getStencilCLIConfig().logger = updatedLogger

And the helper methods pretty much only try to help compensate line length.

Don't worry, I'm not married to this interaction, but at a minimum the singleton definitely helps.

We could get rid of the helper functions quickly, if you'd like me to do that let me know, I'm happy to!

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
`);
};
8 changes: 8 additions & 0 deletions src/cli/telemetry/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,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