Skip to content

Commit

Permalink
feat(cli): [STENCIL-12] Adding Telemetry and CLI for users to toggle …
Browse files Browse the repository at this point in the history
…Telemetry
  • Loading branch information
splitinfinities committed Jul 14, 2021
1 parent 744c9a2 commit 0ec5bbd
Show file tree
Hide file tree
Showing 9 changed files with 1,820 additions and 42 deletions.
1,455 changes: 1,417 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@
"test.watch": "jest --watch"
},
"devDependencies": {
"@ionic/cli-framework-output": "^2.2.2",
"@ionic/utils-subprocess": "^2.1.8",
"@rollup/plugin-commonjs": "15.1.0",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "9.0.0",
"@rollup/plugin-replace": "2.3.4",
"@rollup/pluginutils": "4.1.0",
"@types/autoprefixer": "^10.2.0",
"@types/debug": "^4.1.6",
"@types/exit": "^0.1.31",
"@types/fs-extra": "^9.0.8",
"@types/glob": "^7.1.2",
Expand All @@ -73,6 +76,9 @@
"@types/prompts": "^2.0.9",
"@types/semver": "^7.3.4",
"@types/sizzle": "^2.3.2",
"@types/slice-ansi": "^5.0.0",
"@types/superagent": "^4.1.3",
"@types/superagent-proxy": "^2.0.0",
"@types/webpack": "^4.41.26",
"@types/ws": "^7.4.0",
"ansi-colors": "4.1.1",
Expand All @@ -81,7 +87,9 @@
"conventional-changelog-cli": "^2.1.1",
"core-js-builder": "~3.6.5",
"css": "^3.0.0",
"debug": "^4.3.1",
"dts-bundle-generator": "~5.3.0",
"env-paths": "^2.2.1",
"execa": "4.1.0",
"exit": "^0.1.2",
"fast-deep-equal": "3.1.3",
Expand Down Expand Up @@ -111,6 +119,8 @@
"semiver": "^1.1.0",
"semver": "7.3.4",
"sizzle": "^2.3.6",
"superagent": "^5.2.1",
"superagent-proxy": "^2.0.0",
"terser": "5.6.1",
"tslib": "^2.1.0",
"typescript": "4.2.3",
Expand Down
26 changes: 23 additions & 3 deletions src/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,24 @@ import { taskInfo } from './task-info';
import { taskPrerender } from './task-prerender';
import { taskServe } from './task-serve';
import { taskTest } from './task-test';
import { taskTelemetry } from './task-telemetry';

import { receive } from './telemetry/ipc';
import { telemetryAction } from './telemetry/telemetry';

process.on('message', receive);

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

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

if (flags.debug || flags.verbose) {
logger.setLevel('debug');
}

if (flags.ci) {
logger.enableColors(false);
}
Expand Down Expand Up @@ -67,7 +74,11 @@ export const run = async (init: CliInitOptions) => {
loadedCompilerLog(sys, logger, flags, coreCompiler);

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

Expand All @@ -93,7 +104,12 @@ export const run = async (init: CliInitOptions) => {

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

await runTask(coreCompiler, validated.config, task);
await telemetryAction({
args, sys, logger, config: validated.config,
action: 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 @@ -107,6 +123,10 @@ export const runTask = async (coreCompiler: CoreCompiler, config: Config, task:
config.outputTargets = config.outputTargets || [];

switch (task) {
case 'telemetry':
await taskTelemetry(coreCompiler, config)
break;

case 'build':
await taskBuild(coreCompiler, config);
break;
Expand Down
35 changes: 35 additions & 0 deletions src/cli/task-telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Config} from '../declarations';
import { CoreCompiler } from './load-compiler';
import { checkTelemetry, disableTelemetry, enableTelemetry } from './telemetry/telemetry';

export const taskTelemetry = async (_coreCompiler: CoreCompiler, config: Config) => {
const p = config.logger.dim(config.sys.details.platform === 'windows' ? '>' : '$');
const logger = config.logger;
const isEnabling = config.flags.args.includes("on");
const isDisabling = config.flags.args.includes("off");
const hasTelemetry = await checkTelemetry();
const INFORMATION = `\n\nInformation about the data we collect is available on our website: ${logger.bold('https://stenciljs.com/telemetry')}`
const THANK_YOU = `\n\nThank you for helping to make Stencil better! 💖` + INFORMATION;

if (isEnabling) {
await enableTelemetry()
console.log(`${logger.bold('Telemetry:')} ${logger.dim('Enabled') + THANK_YOU}\n`)
return;
}

if (isDisabling) {
await disableTelemetry()
console.log(`${logger.bold('Telemetry:')} ${logger.dim('Disabled') + INFORMATION}\n`)
return;
}


console.log(`
${logger.bold('Telemetry:')} ${hasTelemetry ? logger.dim('Enabled') + THANK_YOU : logger.dim('Disabled') + INFORMATION}
${p} ${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
`);
};
23 changes: 23 additions & 0 deletions src/cli/telemetry/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import envPaths from 'env-paths';
import { Logger } from 'src/declarations';

import { isFatal } from './errors';

export const ENV_PATHS = envPaths('capacitor', { suffix: '' });

export type CommanderAction = (...args: any[]) => void | Promise<void>;

export function wrapAction(action: CommanderAction, logger: Logger): CommanderAction {
return async (...args: any[]) => {
try {
await action(...args);
} catch (e) {
if (isFatal(e)) {
process.exitCode = e.exitCode;
logger.error(e.message);
} else {
throw e;
}
}
};
}
19 changes: 19 additions & 0 deletions src/cli/telemetry/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export abstract class BaseException<T> extends Error {
constructor (readonly message: string, readonly code: T) {
super(message);
}
}

export class FatalException extends BaseException<'FATAL'> {
constructor (readonly message: string, readonly exitCode = 1) {
super(message, 'FATAL');
}
}

export function fatal(message: string): never {
throw new FatalException(message);
}

export function isFatal(e: any): e is FatalException {
return e && e instanceof FatalException;
}
88 changes: 88 additions & 0 deletions src/cli/telemetry/ipc.ts
Original file line number Diff line number Diff line change
@@ -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 { Metric } from './telemetry';
import { ENV_PATHS } from './cli';

const debug = Debug('stencil:ipc');

export interface TelemetryIPCMessage {
type: 'telemetry';
data: Metric<string, unknown>;
}

export type IPCMessage = TelemetryIPCMessage;

/**
* Send an IPC message to a forked process.
*/
export async function send(msg: IPCMessage): Promise<void> {
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<void> {
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.ionicjs.com',
port: 443,
path: '/events/metrics',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
response => {
debug(
'Sent %O metric to events service (status: %O)',
data.name,
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));
}
}
Loading

0 comments on commit 0ec5bbd

Please sign in to comment.