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

Conversation

splitinfinities
Copy link
Contributor

@splitinfinities splitinfinities commented Jul 14, 2021

Adds Telemetry options to the CLI. Try it out by running any of these commands in your linked project:

npx stencil telemetry
npx stencil telemetry on|off

The debug flag will print the telemetry results, e.g:
npx stencil generate a-component --debug --verbose

Satisfies ADR-1

src/cli/telemetry/ipc.ts Outdated Show resolved Hide resolved
@splitinfinities splitinfinities force-pushed the chore/telemetry/STENCIL-12-add-telemetry branch 2 times, most recently from 0ec5bbd to 276a2fb Compare July 14, 2021 19:14
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
@splitinfinities splitinfinities force-pushed the chore/telemetry/STENCIL-12-add-telemetry branch 4 times, most recently from 5e24abc to 50b2792 Compare July 21, 2021 14:58
package.json Outdated Show resolved Hide resolved
src/declarations/stencil-public-compiler.ts Outdated Show resolved Hide resolved
src/declarations/stencil-public-compiler.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/task-telemetry.ts Outdated Show resolved Hide resolved
src/cli/task-telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
@rwaskiewicz
Copy link
Member

@splitinfinities setting a reminder for both of us to reset the target branch on Monday. Have a great weekend!

@splitinfinities splitinfinities force-pushed the chore/telemetry/STENCIL-12-add-telemetry branch from 50b2792 to 628c0c7 Compare July 26, 2021 18:26
@splitinfinities splitinfinities changed the base branch from chore/telemetry/STENCIL-33-writing-and-reading-config to master July 27, 2021 16:15
const config = await readConfig();
await writeConfig(Object.assign(config, newOptions));
return await writeConfig(Object.assign(config, newOptions));
Copy link
Member

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
Member

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
Member

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

src/cli/task-help.ts Outdated Show resolved Hide resolved
src/cli/task-help.ts Show resolved Hide resolved

export const taskHelp = (sys: CompilerSystem, logger: Logger) => {
const p = logger.dim(sys.details.platform === 'windows' ? '>' : '$');
export const taskHelp = async (config: Config) => {
Copy link
Member

Choose a reason for hiding this comment

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

Is there a need to pass Config as an argument here, could we instead call getCompilerSystem() and getLogger() on stencil-cli-config.ts? What's the preferred interface to use for this singleton?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would love to migrate to use the singleton in more cases like what you point out - I just didn't want to change too much. I personally enjoy the ergonomics of function calls without any props, as you can tell from the code I added.

For the singleton, my thought process was, as we come in and make modifications, let's refactor code to use the singleton more often. I'm happy to go farther, but want to keep this PR scoped closer to Telemetry than general cleanup and ergonomics.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See #2964 (comment) - I switched this out.

src/cli/task-telemetry.ts Outdated Show resolved Hide resolved
Comment on lines 79 to 80
arguments: flags.args,
task: flags.task,
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible that flags can be undefined? I thought that's why we used optional chaining here. Maybe I'm missing something (likely)? Otherwise I'd be concerned the compiler is missing something there 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed that optional chain.

Copy link
Member

@rwaskiewicz rwaskiewicz Jul 28, 2021

Choose a reason for hiding this comment

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

Does that solve the issue though? We define _flags as:

export default class StencilCLIConfig {
  static instance: StencilCLIConfig;

  private _flags: ConfigFlags | undefined;

  private constructor(options: StencilCLIConfigArgs) {
    this._flags = options?.flags || undefined;
  }

  public static getInstance(options?: StencilCLIConfigArgs): StencilCLIConfig {
    if (!StencilCLIConfig.instance) {
      StencilCLIConfig.instance = new StencilCLIConfig(options);
    }

    return StencilCLIConfig.instance;
  }

  public get flags() {
    return this._flags;
  }
  public set flags(flags: ConfigFlags) {
    this._flags = flags;
  }

so it's possible we called the ctor for the first time with an empty object literal

const baz = getInstance({});

which sets _flags to undefined right? Barring setting that somewhere else, this would be an unsafe access

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Considering that use case you mentioned couldn't be possible as the code is written today, I don't think we'll experience that issue.

src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/test/telemetry.spec.ts Outdated Show resolved Hide resolved
splitinfinities and others added 6 commits July 27, 2021 13:33
Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
Co-authored-by: Ryan Waskiewicz <ryanwaskiewicz@gmail.com>
src/cli/run.ts Outdated Show resolved Hide resolved
src/cli/run.ts Show resolved Hide resolved
src/cli/task-help.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
Co-authored-by: Lars Mikkelsen <lars@ionic.io>
@splitinfinities splitinfinities requested a review from a team July 28, 2021 15:52
Co-authored-by: Lars Mikkelsen <lars@ionic.io>
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Show resolved Hide resolved
src/cli/telemetry/telemetry.ts Outdated Show resolved Hide resolved
src/cli/ionic-config.ts Outdated Show resolved Hide resolved
Comment on lines 152 to 154
if (type === 'www' && (!!target?.serviceWorker || target.baseUrl !== '/')) {
type = `${type} (PWA/App)`;
}
Copy link
Member

Choose a reason for hiding this comment

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

Rather than overloading the output targets, I'd add a separate property (e.g. has_app_target) which you might be able to implement as:

function hasAppTarget(config: Config): boolean {
  return config.outputTargets.some(target => target.type === 'www' && (!!target.serviceWorker || target.baseUrl !== '/'));
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ohhh, that's a great idea!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added it as has_app_pwa_config on TrackableData.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See c251962

src/cli/test/ionic-config.spec.ts Show resolved Hide resolved
@@ -21,6 +21,10 @@ export async function readConfig(): Promise<TelemetryConfig> {
};

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

Choose a reason for hiding this comment

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

FYI/no action required - from a TypeScript typing perspective, there is a subtle difference between using Object.assign and the spread operator. Object.assign is typed such that the return type is the intersection of the two args:

assign<T, U>(target: T, source: U): T & U;

if we were to mess up the name of the key in U, the compiler wouldn't catch the error:

// compiles just fine
await writeConfig(Object.assign(config, { 'tokens.teleasdf': newUuid }));

whereas spreading the object will catch an error

// errors! we can't assign to type `TelemetryConfig`
await writeConfig({ ...config, 'tokens.teleasdf': newUuid });

There are some subtle differences between the two (see 'Differences Versus Object.assign()') but I don't think they apply here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you. I did this because I wanted to modify the object in place, but the TypeScript side effect is news to me... Would you like to add prefer-object-spread: error to our ESLint rules? Open to either option.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe WRT prefer-object-spread, I need to start having that conversation with Lars this week about long term linting & formatting plans

@@ -141,21 +144,18 @@ export async function telemetryAction(action?: TelemetryCallback) {
}
}

function hasAppTarget(): boolean {
Copy link
Member

Choose a reason for hiding this comment

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

Can we get this under test please?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was under the impression that you only wanted exported members while we're getting started on coverage. Is that your goal?

Copy link
Member

Choose a reason for hiding this comment

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

That's true in that I don't believe we should be exposing functions solely for the sake of testing (there's always exceptions to the rule 😛). But! The data we're sending has now changed: we have this has_app_pwa_config field whose value is predicated on the evaluation of this function body:

getStencilCLIConfig().validatedConfig.config.outputTargets.some(
  target => target.type === 'www' && (!!target.serviceWorker || target.baseUrl !== '/'),
);

So we should be able to assert that given some outputTargets on the config, that this function's various branches set the desired result in has_app_pwa_config. e.g.

  • false when the output target list is empty
  • false when there is no target type of 'www'
  • false when there is a target type of 'www' & both target.serviceWorker is false and baseUrl is '/'
  • truewhen there is a target type of 'www' & both target.serviceWorker is true and baseUrl is '/'
  • truewhen there is a target type of 'www' & both target.serviceWorker is false and baseUrl is not '/'

Copy link
Member

@rwaskiewicz rwaskiewicz left a comment

Choose a reason for hiding this comment

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

Approved with a few minor asks

export const prepareData = async (duration_ms: number, component_count: number = undefined): Promise<TrackableData> => {
const { flags, sys } = getStencilCLIConfig();
const { typescript, rollup } = getCoreCompiler()?.versions || { typescript: 'unknown', rollup: 'unknown' };
const packages = await getInstalledPackages() || [];
Copy link
Member

Choose a reason for hiding this comment

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

I think this always returns an array, and don't think we need the || [] here

});
});

describe('hasAppTarget', () => {
Copy link
Member

Choose a reason for hiding this comment

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

This outer level describe matches the one on line 80. Did you mean 'prepareData'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did!

expect(telemetry.hasAppTarget()).toBe(true)
});

it('Result is correct when outputTargets contains www with serviceWorker', () => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this test name needs to be updated, there's no service worker field set in this test

expect(telemetry.hasAppTarget()).toBe(true)
});

it('Result is correct when outputTargets contains www with serviceWorker', () => {
Copy link
Member

Choose a reason for hiding this comment

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

I think this test name needs to be updated, it mimics the two previous test names

@rwaskiewicz rwaskiewicz merged commit 1381cc7 into master Aug 4, 2021
@rwaskiewicz rwaskiewicz deleted the chore/telemetry/STENCIL-12-add-telemetry branch August 4, 2021 16:48
rwaskiewicz pushed a commit that referenced this pull request Aug 17, 2021
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants