Skip to content

Commit

Permalink
feat: add TelemetryService
Browse files Browse the repository at this point in the history
  • Loading branch information
chengcyber committed Feb 21, 2024
1 parent dcb88b2 commit 2fc4881
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 19 deletions.
26 changes: 22 additions & 4 deletions apps/sparo-lib/src/api/Sparo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { SparoCommandLine } from '../cli/SparoCommandLine';
import { SparoCICommandLine } from '../cli/SparoCICommandLine';
import type { ICollectTelemetryFunction } from '../services/TelemetryService';

/**
* Options to pass to the sparo "launch" functions.
*
* @public
*/
export interface ILaunchOptions {
/**
* A callback function to tell Sparo how to handle telemetry
* @internal
*
* @remarks
* This is a temporary implementation for customizing telemetry reporting.
* Later, the API will be redesigned to meet more generic requirements.
*/
collectTelemetryAsync?: ICollectTelemetryFunction;
}

/**
* General operations for Sparo engine.
Expand All @@ -9,11 +27,11 @@ import { SparoCICommandLine } from '../cli/SparoCICommandLine';
export class Sparo {
private constructor() {}

public static async launchSparoAsync(): Promise<void> {
await SparoCommandLine.launchAsync();
public static async launchSparoAsync(launchOptions: ILaunchOptions): Promise<void> {
await SparoCommandLine.launchAsync(launchOptions);
}

public static async launchSparoCIAsync(): Promise<void> {
await SparoCICommandLine.launchAsync();
public static async launchSparoCIAsync(launchOptions: ILaunchOptions): Promise<void> {
await SparoCICommandLine.launchAsync(launchOptions);
}
}
12 changes: 10 additions & 2 deletions apps/sparo-lib/src/cli/SparoCICommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ import { ICommand } from './commands/base';
import { ArgvService } from '../services/ArgvService';
import { CIHelpCommand } from './commands/ci-help';
import { GitVersionCompatibility } from '../logic/GitVersionCompatibility';
import { TelemetryService } from '../services/TelemetryService';
import { getCommandName } from './commands/util';
import type { ILaunchOptions } from '../api/Sparo';

export class SparoCICommandLine {
private _commandsMap: Set<string> = new Set<string>();

private constructor() {}

public static async launchAsync(): Promise<void> {
public static async launchAsync(launchOptions: ILaunchOptions): Promise<void> {
await GitVersionCompatibility.ensureGitVersionAsync();

if (launchOptions.collectTelemetryAsync) {
const telemetryService: TelemetryService = await getFromContainer(TelemetryService);
telemetryService.setCollectTelemetryFunction(launchOptions.collectTelemetryAsync);
}

const sparoCI: SparoCICommandLine = new SparoCICommandLine();
await sparoCI.prepareCommandAsync();
await sparoCI.runAsync();
Expand All @@ -28,7 +36,7 @@ export class SparoCICommandLine {
registerClass(cmd);
const cmdInstance: ICommand<{}> = await getFromContainer(cmd);
commandsService.register(cmdInstance);
this._commandsMap.add(cmdInstance.cmd);
this._commandsMap.add(getCommandName(cmdInstance.cmd));
})
);
}
Expand Down
12 changes: 10 additions & 2 deletions apps/sparo-lib/src/cli/SparoCommandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ import { COMMAND_LIST } from './commands/cmd-list';
import { HelpCommand } from './commands/help';
import { ICommand } from './commands/base';
import { GitVersionCompatibility } from '../logic/GitVersionCompatibility';
import { TelemetryService } from '../services/TelemetryService';
import { getCommandName } from './commands/util';
import type { ILaunchOptions } from '../api/Sparo';

export class SparoCommandLine {
private _commandsMap: Set<string> = new Set<string>();

private constructor() {}

public static async launchAsync(): Promise<void> {
public static async launchAsync(launchOptions: ILaunchOptions): Promise<void> {
await GitVersionCompatibility.ensureGitVersionAsync();

if (launchOptions.collectTelemetryAsync) {
const telemetryService: TelemetryService = await getFromContainer(TelemetryService);
telemetryService.setCollectTelemetryFunction(launchOptions.collectTelemetryAsync);
}

const sparo: SparoCommandLine = new SparoCommandLine();
await sparo.prepareCommandAsync();
await sparo.runAsync();
Expand All @@ -29,7 +37,7 @@ export class SparoCommandLine {
registerClass(cmd);
const cmdInstance: ICommand<{}> = await getFromContainer(cmd);
commandsService.register(cmdInstance);
this._commandsMap.add(cmdInstance.cmd);
this._commandsMap.add(getCommandName(cmdInstance.cmd));
})
);
}
Expand Down
9 changes: 9 additions & 0 deletions apps/sparo-lib/src/cli/commands/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import path from 'path';
import childProcess from 'child_process';

/**
* The string of cmd here can be "clone <repository> [directory]", this function extracts
* the command name from the string.
*/
export function getCommandName(cmd: string): string {
const commandName: string = cmd.split(' ')[0];
return commandName;
}

// parse CLI program name (as invoked)
export function prog(): string {
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down
5 changes: 5 additions & 0 deletions apps/sparo-lib/src/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ const defaultContainer: IContainer = new (class {
}
})();

/**
* Get instance from container
*
* @alpha
*/
export async function getFromContainer<T>(clazz: Constructable<T>): Promise<T> {
const instance: T = await defaultContainer.getAsync<T & IAppClassInterface>(clazz);
return instance as T;
Expand Down
8 changes: 7 additions & 1 deletion apps/sparo-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { Sparo } from './api/Sparo';
export { Sparo, type ILaunchOptions } from './api/Sparo';

export { getFromContainer } from './di/container';

export { GitService, type IExecuteGitCommandParams } from './services/GitService';

export type { ICollectTelemetryFunction, ITelemetryData } from './services/TelemetryService';
21 changes: 16 additions & 5 deletions apps/sparo-lib/src/services/CommandService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { LogService } from './LogService';
import { Service } from '../decorator';
import { ArgvService } from './ArgvService';
import { Stopwatch } from '../logic/Stopwatch';
import { TelemetryService } from './TelemetryService';
import { getCommandName } from '../cli/commands/util';

export interface ICommandServiceParams {
yargs: Argv<{}>;
Expand All @@ -18,13 +20,15 @@ export class CommandService {
@inject(ArgvService) private _yargs!: ArgvService;
@inject(HelpTextService) private _helpTextService!: HelpTextService;
@inject(LogService) private _logService!: LogService;
@inject(TelemetryService) private _telemetryService!: TelemetryService;

public register<O extends {}>(command: ICommand<O>): void {
const { cmd: name, description, builder, handler, getHelp } = command;
const { cmd, description, builder, handler, getHelp } = command;
const { _logService: logService } = this;
const { logger } = logService;
const commandName: string = getCommandName(cmd);
this._yargs.yargsArgv.command<O>(
name,
cmd,
description,
(yargs: Argv<{}>) => {
yargs.version(false);
Expand All @@ -33,11 +37,18 @@ export class CommandService {
async (args) => {
process.exitCode = 1;
try {
logger.silly(`invoke command "%s" with args %o`, name, args);
logger.silly(`invoke command "%s" with args %o`, commandName, args);
const stopwatch: Stopwatch = Stopwatch.start();
await handler(args, logService);
logger.silly(`invoke command "%s" done (%s)`, name, stopwatch.toString());
logger.silly(`invoke command "%s" done (%s)`, commandName, stopwatch.toString());
stopwatch.stop();
this._telemetryService.collectTelemetry({
commandName,
args: process.argv.slice(2),
durationInSeconds: stopwatch.duration,
startTimestampMs: stopwatch.startTime,
endTimestampMs: stopwatch.endTime
});
// eslint-disable-next-line require-atomic-updates
process.exitCode = 0;
} catch (e) {
Expand All @@ -47,6 +58,6 @@ export class CommandService {
}
}
);
this._helpTextService.set(name, getHelp());
this._helpTextService.set(commandName, getHelp());
}
}
26 changes: 26 additions & 0 deletions apps/sparo-lib/src/services/GitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@ import { inject } from 'inversify';
import { Service } from '../decorator';
import { LogService } from './LogService';
import { Stopwatch } from '../logic/Stopwatch';
import { TelemetryService } from './TelemetryService';

export interface IGitServiceParams {
logService: LogService;
}

/**
* @alpha
*/
export interface IExecuteGitCommandParams {
args: string[];
workingDirectory?: string;
dryRun?: boolean;
}

/**
* Help class for git operations
*
* @alpha
*/
@Service()
export class GitService {
private _checkedGitPath: boolean = false;
Expand All @@ -24,6 +33,7 @@ export class GitService {
private _gitEmail: string | undefined;
private _isSparseCheckoutMode: boolean | undefined;
@inject(LogService) private _logService!: LogService;
@inject(TelemetryService) private _telemetryService!: TelemetryService;

public setGitConfig(
k: string,
Expand Down Expand Up @@ -207,6 +217,14 @@ export class GitService {
});
this._logService.logger.debug('invoke git command done (%s)', stopwatch.toString());
stopwatch.stop();
this._telemetryService.collectTelemetry({
commandName: args[0],
args: args.slice(1),
durationInSeconds: stopwatch.duration,
startTimestampMs: stopwatch.startTime,
endTimestampMs: stopwatch.endTime,
isRawGitCommand: true
});
return result;
} else {
this._logService.logger.debug('skip running because of dry run mode');
Expand All @@ -231,6 +249,14 @@ export class GitService {
});
this._logService.logger.debug('invoke git command done (%s)', stopwatch.toString());
stopwatch.stop();
this._telemetryService.collectTelemetry({
commandName: args[0],
args: args.slice(1),
durationInSeconds: stopwatch.duration,
startTimestampMs: stopwatch.startTime,
endTimestampMs: stopwatch.endTime,
isRawGitCommand: true
});
this._processResult(result);
return result.stdout.toString();
} else {
Expand Down
82 changes: 82 additions & 0 deletions apps/sparo-lib/src/services/TelemetryService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { Service } from '../decorator';

/**
* @alpha
*/
export interface ITelemetryData {
/**
* Command name
* @example clone
*/
readonly commandName: string;

/**
* Argument list
*/
readonly args: string[];

/**
* Duration in seconds
*/
readonly durationInSeconds: number;

/**
* A timestamp in milliseconds (from `performance.now()`) when the operation started.
* If the operation was blocked, will be `undefined`.
*/
readonly startTimestampMs?: number;

/**
* A timestamp in milliseconds (from `performance.now()`) when the operation finished.
* If the operation was blocked, will be `undefined`.
*/
readonly endTimestampMs?: number;

/**
* Indicates raw git command
*/
readonly isRawGitCommand?: boolean;
}

/**
* @alpha
*/
export type ICollectTelemetryFunction = (data: ITelemetryData) => Promise<void>;

/**
* A help class to collect telemetry. The way to collect data is specified during launch Sparo.
* NOTE: In this release version, we do NOT upload any telemetry data. It's under test internally
* and the API will be refactored to meet generic requirements later.
*/
@Service()
export class TelemetryService {
private _tasks: Set<Promise<void>> = new Set();

private _collectTelemetryAsyncMaybe: ICollectTelemetryFunction | undefined;

/**
* This is a sync function. Collecting telemetry should not block anything.
* In the end of process, "ensureCollectedAsync" should be called.
*/
public collectTelemetry(data: ITelemetryData): void {
if (this._collectTelemetryAsyncMaybe) {
const task: Promise<void> = this._collectTelemetryAsyncMaybe(data);
this._tasks.add(task);
task
.then(() => {
this._tasks.delete(task);
})
.catch(() => {
this._tasks.delete(task);
});
}
}

public async ensureCollectedAsync(): Promise<void> {
await Promise.all(this._tasks);
}

public setCollectTelemetryFunction(fn: ICollectTelemetryFunction): void {
this._collectTelemetryAsyncMaybe = fn;
}
}
7 changes: 4 additions & 3 deletions apps/sparo/src/SparoCommandSelector.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import * as path from 'path';

import { Sparo } from 'sparo-lib';
import { Sparo, type ILaunchOptions } from 'sparo-lib';

type CommandName = 'sparo' | 'sparo-ci' | undefined;

export class SparoCommandSelector {
public static async executeAsync(): Promise<void> {
const commandName: CommandName = SparoCommandSelector._getCommandName();
const launchOptions: ILaunchOptions = {};
switch (commandName) {
case 'sparo-ci': {
await Sparo.launchSparoCIAsync();
await Sparo.launchSparoCIAsync(launchOptions);
break;
}
default: {
await Sparo.launchSparoAsync();
await Sparo.launchSparoAsync(launchOptions);
break;
}
}
Expand Down
Loading

0 comments on commit 2fc4881

Please sign in to comment.