diff --git a/packages/amplify-category-api/src/index.ts b/packages/amplify-category-api/src/index.ts index 841fefc9fd9..94e39cc6d9a 100644 --- a/packages/amplify-category-api/src/index.ts +++ b/packages/amplify-category-api/src/index.ts @@ -235,7 +235,7 @@ export async function executeAmplifyCommand(context: $TSContext) { } export const executeAmplifyHeadlessCommand = async (context: $TSContext, headlessPayload: string) => { - context.flowData?.pushHeadlessFlow(headlessPayload); + context.flowData.pushHeadlessFlow(headlessPayload, context.input); switch (context.input.command) { case 'add': await getCfnApiArtifactHandler(context).createArtifacts(await validateAddApiRequest(headlessPayload)); diff --git a/packages/amplify-category-auth/src/index.js b/packages/amplify-category-auth/src/index.js index 998ec88ec5e..c7e46ad5bb7 100644 --- a/packages/amplify-category-auth/src/index.js +++ b/packages/amplify-category-auth/src/index.js @@ -352,7 +352,7 @@ async function initEnv(context) { await sequential(authTasks); } -async function console(context) { +async function authConsole(context) { const { amplify } = context; const amplifyMeta = amplify.getProjectMeta(); @@ -425,7 +425,7 @@ async function executeAmplifyCommand(context) { * @param {string} headlessPayload The serialized payload from the platform */ const executeAmplifyHeadlessCommand = async (context, headlessPayload) => { - context.flowData.pushHeadlessFlow(headlessPayload); + context.flowData.pushHeadlessFlow(headlessPayload, context.input); switch (context.input.command) { case 'add': if (projectHasAuth(context)) { @@ -516,7 +516,7 @@ module.exports = { add, migrate, initEnv, - console, + console : authConsole, getPermissionPolicies, executeAmplifyCommand, executeAmplifyHeadlessCommand, diff --git a/packages/amplify-category-geo/src/index.ts b/packages/amplify-category-geo/src/index.ts index d6ca14a93cf..bd622170c7f 100644 --- a/packages/amplify-category-geo/src/index.ts +++ b/packages/amplify-category-geo/src/index.ts @@ -72,8 +72,8 @@ export const getPermissionPolicies = (context: $TSContext, resourceOpsMapping: $ * @param {any} context The amplify context object * @param {string} headlessPayload The serialized payload from the platform */ - export const executeAmplifyHeadlessCommand = async (context: $TSContext, headlessPayload: string) => { - context.flowData?.pushHeadlessFlow(headlessPayload); +export const executeAmplifyHeadlessCommand = async (context: $TSContext, headlessPayload: string) => { + context.flowData.pushHeadlessFlow(headlessPayload, context.input); switch (context.input.command) { case 'add': await addResourceHeadless(context, headlessPayload); diff --git a/packages/amplify-category-storage/src/index.ts b/packages/amplify-category-storage/src/index.ts index e93edaeaa9c..5285f81ddaf 100644 --- a/packages/amplify-category-storage/src/index.ts +++ b/packages/amplify-category-storage/src/index.ts @@ -186,7 +186,7 @@ export async function executeAmplifyCommand(context: any) { } export const executeAmplifyHeadlessCommand = async (context: $TSContext, headlessPayload: string) => { - context.flowData?.pushHeadlessFlow(headlessPayload); + context.flowData.pushHeadlessFlow(headlessPayload, context.input); switch (context.input.command) { case 'add': await headlessAddStorage(context, await validateAddStorageRequest(headlessPayload)); diff --git a/packages/amplify-cli-shared-interfaces/src/amplify-cli-flow-reporter-types.ts b/packages/amplify-cli-shared-interfaces/src/amplify-cli-flow-reporter-types.ts index fccb09a5e22..5836ddfc6a2 100644 --- a/packages/amplify-cli-shared-interfaces/src/amplify-cli-flow-reporter-types.ts +++ b/packages/amplify-cli-shared-interfaces/src/amplify-cli-flow-reporter-types.ts @@ -1,13 +1,13 @@ import { ICommandInput } from './amplify-cli-interactions'; -export interface IOptionFlowHeadlessData { - input: string, - timestamp: number +export interface IOptionFlowHeadlessData { + input: string, + timestamp: number } export interface IOptionFlowCLIData { - prompt: string, - input: unknown, - timestamp: number + prompt: string, + input: unknown, + timestamp: number } export type TypeOptionFlowData = IOptionFlowHeadlessData | IOptionFlowCLIData @@ -17,17 +17,17 @@ export type TypeOptionFlowData = IOptionFlowHeadlessData | IOptionFlowCLIData * Flow Report data logged by the CLI walk-through. */ export interface IFlowReport { - version : string, - runtime : string, - executable : string, - category : string, - isHeadless : boolean, - cmd : string, - subCmd: string| undefined, - optionFlowData : Array, //IOptionFlowHeadlessData | IOptionFlowCLIData - input : ICommandInput, - timestamp : string, - projectEnvIdentifier? : string, // hash(ProjectName + Amplify AppId + EnvName) + version: string, + runtime: string, + executable: string, + category: string, + isHeadless: boolean, + cmd: string, + subCmd: string | undefined, + optionFlowData: Array, //IOptionFlowHeadlessData | IOptionFlowCLIData + input: ICommandInput, + timestamp: string, + projectEnvIdentifier?: string, // hash(ProjectName + Amplify AppId + EnvName) projectIdentifier?: string, // hash( ProjectName + Amplify App Id) } @@ -35,9 +35,9 @@ export interface IFlowReport { * CLI walk-through and headless flow data */ export interface IFlowData { - setIsHeadless : (headless: boolean)=> void, - pushHeadlessFlow: (headlessFlowDataString: string) => void, + setIsHeadless: (headless: boolean) => void, + pushHeadlessFlow: (headlessFlowDataString: string, input: ICommandInput) => void, pushInteractiveFlow: (prompt: string, input: unknown) => void, - getFlowReport: ()=>IFlowReport | Record - assignProjectIdentifier: (envName?:string)=>string|undefined + getFlowReport: () => IFlowReport | Record + assignProjectIdentifier: (envName?: string) => string | undefined } diff --git a/packages/amplify-cli/src/__tests__/flow-report.test.ts b/packages/amplify-cli/src/__tests__/flow-report.test.ts index 9b96add6fa7..f64b44b7e07 100644 --- a/packages/amplify-cli/src/__tests__/flow-report.test.ts +++ b/packages/amplify-cli/src/__tests__/flow-report.test.ts @@ -1,9 +1,12 @@ import { CLIFlowReport } from '../domain/amplify-usageData/FlowReport'; import { stateManager } from 'amplify-cli-core'; -import { AddS3ServiceConfiguration, AddStorageRequest, - ImportAuthRequest, CrudOperation, AddGeoRequest, - GeoServiceConfiguration, AccessType, MapStyle, AppSyncServiceConfiguration, AppSyncAPIKeyAuthType, AddApiRequest} from 'amplify-headless-interface'; +import { + AddS3ServiceConfiguration, AddStorageRequest, + ImportAuthRequest, CrudOperation, AddGeoRequest, + GeoServiceConfiguration, AccessType, MapStyle, AppSyncServiceConfiguration, AppSyncAPIKeyAuthType, AddApiRequest +} from 'amplify-headless-interface'; import { v4 as uuid } from 'uuid'; +import { Redactor } from 'amplify-cli-logger'; @@ -16,106 +19,113 @@ describe('Test FlowReport Logging', () => { // CLIFlowReport.reset(); jest.clearAllMocks(); }); - + it('flow-log-interactive-payload: Interactive payload is available in payload', () => { + const flowReport = CLIFlowReport.instance; + flowReport.pushInteractiveFlow(mockInputs.interactivePrompt, mockInputs.interactiveValue); + expect(flowReport.optionFlowData[0].input).toContain(mockInputs.interactiveValue); + }) it('Project Identifiers are correctly formed', () => { - const flowReport = CLIFlowReport.instance; - flowReport.setIsHeadless(true); - //Mock the state-manager functions - jest.mock('amplify-cli-core'); - jest.spyOn(stateManager, 'getProjectName').mockReturnValue(mockInputs.projectName); - jest.spyOn(stateManager, 'getCurrentEnvName').mockReturnValueOnce(mockInputs.envName); - jest.spyOn(stateManager, 'getAppID').mockReturnValue(mockInputs.appID) - flowReport.assignProjectIdentifier(); - expect( flowReport.projectEnvIdentifier ).toEqual(`${mockInputs.projectName}${mockInputs.appID}${mockInputs.envName}`); - expect( flowReport.projectIdentifier ).toEqual(`${mockInputs.projectName}${mockInputs.appID}`); + const flowReport = CLIFlowReport.instance; + flowReport.setIsHeadless(true); + flowReport.setVersion(mockInputs.version); + //Mock the state-manager functions + jest.mock('amplify-cli-core'); + jest.spyOn(stateManager, 'getProjectName').mockReturnValue(mockInputs.projectName); + jest.spyOn(stateManager, 'getCurrentEnvName').mockReturnValueOnce(mockInputs.envName); + jest.spyOn(stateManager, 'getAppID').mockReturnValue(mockInputs.appID) + flowReport.assignProjectIdentifier(); + expect(flowReport.projectEnvIdentifier).toEqual(`${mockInputs.projectName}${mockInputs.appID}${mockInputs.envName}`); + expect(flowReport.projectIdentifier).toEqual(`${mockInputs.projectName}${mockInputs.appID}`); + expect(flowReport.version).toEqual(mockInputs.version); }); - + it('flow-log-headless-payload: (Auth) is redacted in flowReport', () => { - const input = getAuthHeadlessTestInput(); - const inputString = JSON.stringify(input); - const flowReport = CLIFlowReport.instance; - flowReport.setIsHeadless(true); - flowReport.setInput(mockInputs.headlessInput('auth', 'add')); - flowReport.pushHeadlessFlow(inputString); - expect(flowReport.optionFlowData[0].input).not.toContain(input.identityPoolId); - expect(flowReport.optionFlowData[0].input).not.toContain(input.webClientId); - expect(flowReport.optionFlowData[0].input).not.toContain(input.nativeClientId); - const redactedInputString = flowReport.optionFlowData[0].input; - const report = flowReport.getFlowReport(); - expect(report.isHeadless).toEqual(true); - expect(report.input.plugin).toEqual('auth'); - expect(report.input.command).toEqual('add'); - expect(report.optionFlowData[0].input).toEqual(redactedInputString); + const input = getAuthHeadlessTestInput(); + const inputString = JSON.stringify(input); + const flowReport = CLIFlowReport.instance; + flowReport.pushHeadlessFlow(inputString, mockInputs.headlessInput('auth', 'add')); + expect(flowReport.optionFlowData[0].input).not.toContain(input.identityPoolId); + expect(flowReport.optionFlowData[0].input).not.toContain(input.webClientId); + expect(flowReport.optionFlowData[0].input).not.toContain(input.nativeClientId); + const report = flowReport.getFlowReport(); + expect(report.isHeadless).toEqual(true); + expect(report.input.plugin).toEqual('auth'); + expect(report.input.command).toEqual('add'); + expect(report.optionFlowData[0].input).toEqual(Redactor(inputString)); }); - + it('flow-log-headless-payload: (Storage S3) is redacted in flowReport', () => { const input = getAddStorageS3HeadlessTestInput(); const inputString = JSON.stringify(input); const flowReport = CLIFlowReport.instance; - flowReport.setIsHeadless(true); flowReport.assignProjectIdentifier(); - flowReport.setInput(mockInputs.headlessInput('storage', 'add')); - flowReport.pushHeadlessFlow(inputString); + flowReport.pushHeadlessFlow(inputString, mockInputs.headlessInput('storage', 'add')); expect(flowReport.optionFlowData[0].input).not.toContain(input.serviceConfiguration.bucketName); expect(flowReport.optionFlowData[0].input).not.toContain(input.serviceConfiguration.resourceName); expect(flowReport.optionFlowData[0].input).not.toContain(input.serviceConfiguration.lambdaTrigger?.name as string); - const redactedInputString = flowReport.optionFlowData[0].input; + expect(flowReport.optionFlowData[0].input).toContain(redactValue('bucketName', input.serviceConfiguration.bucketName)); + expect(flowReport.optionFlowData[0].input).toContain(redactValue('resourceName', input.serviceConfiguration.resourceName)); + expect(flowReport.optionFlowData[0].input).toContain(redactValue('name', input.serviceConfiguration.lambdaTrigger?.name as string)); const report = flowReport.getFlowReport(); expect(report.isHeadless).toEqual(true); expect(report.input.plugin).toEqual('storage'); expect(report.input.command).toEqual('add'); - expect(report.optionFlowData[0].input).toEqual(redactedInputString); - + expect(report.optionFlowData[0].input).toEqual(Redactor(inputString)); }); - + it('flow-log-headless-payload: (API GraphQL) headless-payload is redacted in flowReport', () => { const input = getGraphQLHeadlessTestInput(); + const inputString = JSON.stringify(input); const flowReport = CLIFlowReport.instance; - flowReport.setIsHeadless(true); flowReport.assignProjectIdentifier(); - flowReport.setInput(mockInputs.headlessInput('api', 'add')); - flowReport.pushHeadlessFlow(inputString); + flowReport.pushHeadlessFlow(inputString, mockInputs.headlessInput('api', 'add')); expect(flowReport.optionFlowData[0].input).not.toContain(input.serviceConfiguration.apiName); //resource name redacted - const redactedInput = JSON.parse( flowReport.optionFlowData[0].input as unknown as string ); + expect(flowReport.optionFlowData[0].input).toContain(redactValue("apiName", input.serviceConfiguration.apiName)); + const redactedInput = JSON.parse(flowReport.optionFlowData[0].input as unknown as string); expect(redactedInput.serviceConfiguration.transformSchema).toEqual(input.serviceConfiguration.transformSchema);//transform schema must exist }); it('flow-log-headless-payload: (Geo) is redacted in flowReport', () => { const input = getGeoHeadlessTestInput(); const inputString = JSON.stringify(input); + const redactedName = redactValue('name', input.serviceConfiguration.name); const flowReport = CLIFlowReport.instance; - flowReport.setIsHeadless(true); flowReport.assignProjectIdentifier(); - flowReport.setInput(mockInputs.headlessInput('geo', 'add')); - flowReport.pushHeadlessFlow(inputString); + flowReport.pushHeadlessFlow(inputString, mockInputs.headlessInput('geo', 'add')); expect(flowReport.optionFlowData[0].input).not.toContain(input.serviceConfiguration.name); //resource name redacted + expect(flowReport.optionFlowData[0].input).toContain(redactedName); //resource name redacted expect(flowReport.optionFlowData[0].input).toContain(input.serviceConfiguration.accessType); expect(flowReport.optionFlowData[0].input).toContain(input.serviceConfiguration.mapStyle); + expect(flowReport.optionFlowData[0].input).toEqual(Redactor(inputString)); }); - }); +}); +const redactValue = (key: string, value: unknown) => { + const retVal = JSON.parse(Redactor(JSON.stringify({ [key]: value }))); + return retVal[key]; +} /*** Helper functions and test data ***/ -const mockAddStorageInput : AddS3ServiceConfiguration = { +const mockAddStorageInput: AddS3ServiceConfiguration = { serviceName: 'S3', permissions: { auth: [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ, CrudOperation.DELETE], guest: [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ], groups: { - Admin : [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ, CrudOperation.DELETE], - Guest : [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ], - Reader : [CrudOperation.READ] + Admin: [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ, CrudOperation.DELETE], + Guest: [CrudOperation.CREATE_AND_UPDATE, CrudOperation.READ], + Reader: [CrudOperation.READ] } }, resourceName: 'testMockS3ResourceName', bucketName: 'testMockS3BucketName', lambdaTrigger: { - mode: 'new', - name: 'existingFunctionName' - }, + mode: 'new', + name: 'existingFunctionName' + }, } const getShortId = (prefix: string) => { @@ -124,7 +134,7 @@ const getShortId = (prefix: string) => { return mapId; } -const mockAddGeoInput : GeoServiceConfiguration = { +const mockAddGeoInput: GeoServiceConfiguration = { serviceName: "Map", name: getShortId('map'), accessType: AccessType.AuthorizedUsers, @@ -132,12 +142,12 @@ const mockAddGeoInput : GeoServiceConfiguration = { setAsDefault: true } -const appSyncAPIKeyAuthType : AppSyncAPIKeyAuthType = { - mode: 'API_KEY', - expirationTime: Math.floor((Date.now()/1000) + 86400), //one day - } +const appSyncAPIKeyAuthType: AppSyncAPIKeyAuthType = { + mode: 'API_KEY', + expirationTime: Math.floor((Date.now() / 1000) + 86400), //one day +} -const mockAddAPIInput : AppSyncServiceConfiguration = { +const mockAddAPIInput: AppSyncServiceConfiguration = { serviceName: 'AppSync', apiName: "mockGQLAPIName", transformSchema: 'type User @model(subscriptions: null)\ @@ -155,38 +165,41 @@ const mockAddAPIInput : AppSyncServiceConfiguration = { const mockInputs = { projectName: 'mockProjectName', - envName : 'dev', - appID : 'mockAppID', + envName: 'dev', + appID: 'mockAppID', Auth: { - USER_POOL_ID : 'user-pool-123', - IDENTITY_POOL_ID : 'identity-pool-123', - NATIVE_CLIENT_ID : 'native-app-client-123', - WEB_CLIENT_ID : 'web-app-client-123' + USER_POOL_ID: 'user-pool-123', + IDENTITY_POOL_ID: 'identity-pool-123', + NATIVE_CLIENT_ID: 'native-app-client-123', + WEB_CLIENT_ID: 'web-app-client-123' }, - StorageS3 : mockAddStorageInput, - Geo : mockAddGeoInput, - GraphQLAPI : mockAddAPIInput, - headlessInput : (feature, command) => ({ - argv : [], - plugin : feature, - command : command - }) + StorageS3: mockAddStorageInput, + Geo: mockAddGeoInput, + GraphQLAPI: mockAddAPIInput, + headlessInput: (feature, command) => ({ + argv: [], + plugin: feature, + command: command + }), + interactivePrompt: "Enter resource name", + interactiveValue: "mockResourceID", + version: "1.0" } const getAuthHeadlessTestInput = () => { - const headlessPayload: ImportAuthRequest = { - version: 1, - userPoolId: mockInputs.Auth.USER_POOL_ID, - identityPoolId: mockInputs.Auth.IDENTITY_POOL_ID, - nativeClientId: mockInputs.Auth.NATIVE_CLIENT_ID, - webClientId: mockInputs.Auth.WEB_CLIENT_ID, - }; - return headlessPayload; + const headlessPayload: ImportAuthRequest = { + version: 1, + userPoolId: mockInputs.Auth.USER_POOL_ID, + identityPoolId: mockInputs.Auth.IDENTITY_POOL_ID, + nativeClientId: mockInputs.Auth.NATIVE_CLIENT_ID, + webClientId: mockInputs.Auth.WEB_CLIENT_ID, + }; + return headlessPayload; } const getAddStorageS3HeadlessTestInput = () => { const headlessPayload: AddStorageRequest = { version: 1, - serviceConfiguration : mockInputs.StorageS3 + serviceConfiguration: mockInputs.StorageS3 }; return headlessPayload; } @@ -209,6 +222,6 @@ const getGraphQLHeadlessTestInput = () => { serviceConfiguration: mockInputs.GraphQLAPI } return headlessPayload; - + } diff --git a/packages/amplify-cli/src/__tests__/version-manager.test.ts b/packages/amplify-cli/src/__tests__/version-manager.test.ts index 995aeada8e5..137fd702770 100644 --- a/packages/amplify-cli/src/__tests__/version-manager.test.ts +++ b/packages/amplify-cli/src/__tests__/version-manager.test.ts @@ -1,8 +1,10 @@ import url from 'url'; import { prodUrl } from '../domain/amplify-usageData/getUsageDataUrl'; import { UsageDataPayload } from '../domain/amplify-usageData/UsageDataPayload'; +import { UsageData } from '../domain/amplify-usageData' import { getLatestApiVersion, getLatestPayloadVersion } from '../domain/amplify-usageData/VersionManager'; import { Input } from '../domain/input'; +import { IFlowReport } from 'amplify-cli-shared-interfaces'; describe('test version manager', () => { it('url version should be the latest URL', () => { @@ -23,6 +25,7 @@ describe('test version manager', () => { { frontend: 'javascript', editor: 'vscode', framework: 'react' }, {}, {}, + UsageData.flowInstance.getFlowReport() as IFlowReport ); expect(payload.payloadVersion).toEqual(getLatestPayloadVersion()); }); diff --git a/packages/amplify-cli/src/domain/amplify-usageData/FlowReport.ts b/packages/amplify-cli/src/domain/amplify-usageData/FlowReport.ts index d4fa041f031..6ec6c9023f8 100644 --- a/packages/amplify-cli/src/domain/amplify-usageData/FlowReport.ts +++ b/packages/amplify-cli/src/domain/amplify-usageData/FlowReport.ts @@ -2,44 +2,46 @@ import { $TSAny, JSONUtilities, stateManager } from 'amplify-cli-core'; import { logger, Redactor } from 'amplify-cli-logger'; import { IAmplifyLogger } from 'amplify-cli-logger/lib/IAmplifyLogger'; -import { IFlowData, IFlowReport, IOptionFlowCLIData, IOptionFlowHeadlessData, TypeOptionFlowData } from 'amplify-cli-shared-interfaces'; +import { ICommandInput, IFlowData, IFlowReport, IOptionFlowCLIData, IOptionFlowHeadlessData, TypeOptionFlowData } from 'amplify-cli-shared-interfaces'; import { Input } from '../input'; /** * Store the data and sequence of events of CLI walkthrough */ export class CLIFlowReport implements IFlowData { - private static _instance: CLIFlowReport = new CLIFlowReport(); - version!: string; - runtime!: string; - executable!: string; - category!: string; - isHeadless!: boolean; - cmd!: string; - subCmd: string| undefined; - optionFlowData!: Array; - logger!: IAmplifyLogger; - input!: Input; - timestamp : string; - projectEnvIdentifier? : string; // hash(ProjectName + Amplify AppId + EnvName) - projectIdentifier?: string; // hash( ProjectName + Amplify App Id) - envName?: string; + private static _instance: CLIFlowReport = new CLIFlowReport(); + version!: string; + runtime!: string; + executable!: string; + category!: string; + isHeadless!: boolean; + cmd!: string; + subCmd: string | undefined; + optionFlowData!: Array; + logger!: IAmplifyLogger; + input!: Input; + timestamp: string; + projectEnvIdentifier?: string; // hash(ProjectName + Amplify AppId + EnvName) + projectIdentifier?: string; // hash( ProjectName + Amplify App Id) + envName?: string; - private constructor() { - const currentTime = Date.now(); - if (CLIFlowReport._instance) { - throw new Error('Use CLIFlowReport.instance'); - } - CLIFlowReport._instance = this; - this.logger = logger; - this.timestamp = currentTime.toString(); - this.isHeadless = false; //set headless to true if running in headless mode : TBD: can we query this from stateManager? + private constructor() { + const currentTime = Date.now(); + if (CLIFlowReport._instance) { + throw new Error('Use CLIFlowReport.instance'); } + CLIFlowReport._instance = this; + this.logger = logger; + this.timestamp = currentTime.toString(); + this.isHeadless = false; //set headless to true if running in headless mode : TBD: can we query this from stateManager? + this.optionFlowData = []; + } - /** - * Initialize the project identifier to be used during the flow - */ - assignProjectIdentifier():undefined|string { + /** + * Initialize the project identifier to be used during the flow + */ + assignProjectIdentifier(): undefined | string { + if (!this.projectIdentifier) { try { const projectName = stateManager.getProjectName(); const envName = stateManager.getCurrentEnvName(); @@ -51,115 +53,96 @@ export class CLIFlowReport implements IFlowData { return undefined; } } + } - /** - * Singleton instance builder - */ - static get instance() : CLIFlowReport { - if (!CLIFlowReport._instance) { - CLIFlowReport._instance = new CLIFlowReport(); - } - return CLIFlowReport._instance; + /** + * Singleton instance builder + */ + static get instance(): CLIFlowReport { + if (!CLIFlowReport._instance) { + CLIFlowReport._instance = new CLIFlowReport(); } + return CLIFlowReport._instance; + } - /** - * Singleton instance resetter //used only for testing - * @param input - */ - static reset() : CLIFlowReport { - if (CLIFlowReport._instance) { - CLIFlowReport._instance = new CLIFlowReport(); - } - CLIFlowReport._instance.timestamp = (new Date()).toString(); - return CLIFlowReport._instance; + /** + * Set the CLI input args + * @param input - first arguments provided in the CLI flow + */ + setInput(input: Input): void { + this.input = input; + this.runtime = input.argv[0] as string; + this.executable = input.argv[1] as string; + this.cmd = input.argv[2] as string; + this.subCmd = (input.argv[3]) ? input.argv[3] : undefined; + this.optionFlowData = []; // key-value store with ordering maintained + // Parse options + if (input.options?.prompt) { + const prompt: string = input.options.prompt as unknown as string; + this.pushInteractiveFlow(prompt, input.options.input); } + } - /** - * Set the CLI input args - * @param input - first arguments provided in the CLI flow - */ - setInput(input: Input):void { - this.input = input; - this.runtime = input.argv[0] as string; - this.executable = input.argv[1] as string; - this.cmd = input.argv[2] as string; - this.subCmd = (input.argv[3]) ? input.argv[3] : undefined; - this.optionFlowData = []; // key-value store with ordering maintained - // Parse options - if (input.options?.prompt) { - const prompt: string = input.options.prompt as unknown as string; - this.pushInteractiveFlow(prompt, input.options.input); - } - } + /** + * Set the Amplify CLI version being used for this flow + */ + setVersion(version: string): void { + this.version = version; + } - /** - * Set the Amplify CLI version being used for this flow - */ - setVersion(version: string):void { - this.version = version; - } + /** + * Returns JSON version of this object + * + * @returns JSON version of the object + */ + getFlowReport(): IFlowReport { + const result: IFlowReport = { + runtime: this.runtime, + executable: this.executable, + version: this.version, + cmd: this.cmd, + subCmd: this.subCmd, + isHeadless: this.isHeadless, + optionFlowData: this.optionFlowData, + category: this.category, + input: this.input, + timestamp: this.timestamp, + projectEnvIdentifier: this.projectEnvIdentifier, + projectIdentifier: this.projectIdentifier, + }; + return result; + } - /** - * Returns JSON version of this object - * - * @returns JSON version of the object - */ - getFlowReport(): IFlowReport { - const result : IFlowReport = { - runtime: this.runtime, - executable: this.executable, - version: this.version, - cmd: this.cmd, - subCmd: this.subCmd, - isHeadless: this.isHeadless, - optionFlowData: this.optionFlowData, - category: this.category, - input: this.input, - timestamp: this.timestamp, - projectEnvIdentifier: this.projectEnvIdentifier, - projectIdentifier: this.projectIdentifier, - }; - return result; - } + /** + * This method is to configure when the current flow is headless. + * @param isHeadless + */ + setIsHeadless(isHeadless: boolean): void { + this.isHeadless = isHeadless; + } - /** - * This method is to configure when the current flow is headless. - * @param isHeadless - */ - setIsHeadless(isHeadless: boolean): void { - this.isHeadless = isHeadless; - } + /** + * This method is called whenever user selects an option in the CLI walkthrough + * @param selectedOption - walkthrough options selected + */ + pushInteractiveFlow(prompt: string, input: unknown): void { + const redactedString = Redactor(JSON.stringify({ prompt, input })); + const cleanOption = JSON.parse(redactedString); + const timeStampedCLIFlowOption: IOptionFlowCLIData = { ...cleanOption, timestamp: new Date().valueOf() }; // attach unix-style timestamp + this.optionFlowData.push(timeStampedCLIFlowOption); + } - /** - * This method is called whenever user selects an option in the CLI walkthrough - * @param selectedOption - walkthrough options selected - */ - pushInteractiveFlow( prompt: string, input: unknown ): void { - const redactedString = Redactor(JSON.stringify({prompt, input})); - const cleanOption = JSON.parse(redactedString); - const timeStampedCLIFlowOption:IOptionFlowCLIData = { ...cleanOption, timestamp: new Date().valueOf() }; // attach unix-style timestamp - this.optionFlowData.push(timeStampedCLIFlowOption); - } + /** + * This method is called whenever the CLI is invoked in headless mode + * @param headlessParameterString - headless parameter string ( serialized but before schema validation ) + */ + pushHeadlessFlow(headlessParameterString: string, cliInput: ICommandInput): void { + this.assignProjectIdentifier(); //conditionally initialize the project identifier + this.setIsHeadless(true); + this.setInput(cliInput); + const cleanOption = Redactor(headlessParameterString); + const timeStampedOption: IOptionFlowHeadlessData = { input: cleanOption, timestamp: new Date().valueOf() }; // attach unix-style timestamp + this.optionFlowData.push(timeStampedOption); + } - /** - * This method is called whenever the CLI is invoked in headless mode - * @param headlessParameterString - headless parameter string ( serialized but before schema validation ) - */ - pushHeadlessFlow(headlessParameterString: string): void { - const cleanOption = Redactor(headlessParameterString); - const timeStampedOption:IOptionFlowHeadlessData = { input: cleanOption, timestamp: new Date().valueOf() }; // attach unix-style timestamp - this.optionFlowData.push(timeStampedOption); - } - - /** - * Log input data to the local file system - */ - logInput(): void { - this.logger.logInfo({ - message: `amplify ${this.input.command ? this.input.command : ''} \ - ${this.input.plugin ? this.input.plugin : ''} \ - ${this.input.subCommands ? this.input.subCommands.join(' ') : ''} \ - ${this.input.options ? Redactor(JSONUtilities.stringify(this.input.options, { minify: true })) : ''}`, - }); - } } diff --git a/packages/amplify-cli/src/domain/amplify-usageData/NoFlowReport.ts b/packages/amplify-cli/src/domain/amplify-usageData/NoFlowReport.ts index 1688ab621af..9528c2b51be 100644 --- a/packages/amplify-cli/src/domain/amplify-usageData/NoFlowReport.ts +++ b/packages/amplify-cli/src/domain/amplify-usageData/NoFlowReport.ts @@ -1,24 +1,24 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import { $TSAny } from 'amplify-cli-core'; -import { IFlowData, IFlowReport, IOptionFlowCLIData, IOptionFlowHeadlessData } from 'amplify-cli-shared-interfaces'; +import { ICommandInput, IFlowData, IFlowReport, IOptionFlowCLIData, IOptionFlowHeadlessData } from 'amplify-cli-shared-interfaces'; /** * No-Op class for flow data logging */ export class CLINoFlowReport implements IFlowData { - private static _instance: CLINoFlowReport = new CLINoFlowReport(); - pushInteractiveFlow: (prompt: string, input: unknown) => void = _ => _ ; - getFlowReport: () => IFlowReport | Record = () => ({}); - assignProjectIdentifier: ()=>string|undefined = () => undefined; - setIsHeadless: (isHeadless: boolean)=>void = _=>_; - pushHeadlessFlow: (headlessFlowDataString: string) => void = _ => _ ; - /** - * No-op instance of the CLINoFlowReport class - */ - static get instance() : CLINoFlowReport { - if (!CLINoFlowReport._instance) { - CLINoFlowReport._instance = new CLINoFlowReport(); - } - return CLINoFlowReport._instance; + private static _instance: CLINoFlowReport = new CLINoFlowReport(); + pushInteractiveFlow: (prompt: string, input: unknown) => void = _ => _; + getFlowReport: () => IFlowReport | Record = () => ({}); + assignProjectIdentifier: () => string | undefined = () => undefined; + setIsHeadless: (isHeadless: boolean) => void = _ => _; + pushHeadlessFlow: (headlessFlowDataString: string, input: ICommandInput) => void = _ => _; + /** + * No-op instance of the CLINoFlowReport class + */ + static get instance(): CLINoFlowReport { + if (!CLINoFlowReport._instance) { + CLINoFlowReport._instance = new CLINoFlowReport(); } + return CLINoFlowReport._instance; + } } diff --git a/packages/amplify-cli/src/domain/amplify-usageData/NoUsageData.ts b/packages/amplify-cli/src/domain/amplify-usageData/NoUsageData.ts index 6cbb3eb1e3e..98a308afbe2 100644 --- a/packages/amplify-cli/src/domain/amplify-usageData/NoUsageData.ts +++ b/packages/amplify-cli/src/domain/amplify-usageData/NoUsageData.ts @@ -2,7 +2,7 @@ /* eslint-disable class-methods-use-this */ import { $TSAny } from 'amplify-cli-core'; import { IFlowReport } from 'amplify-cli-shared-interfaces/lib/amplify-cli-flow-reporter-types'; -import { IFlowData } from 'amplify-cli-shared-interfaces'; +import { ICommandInput, IFlowData } from 'amplify-cli-shared-interfaces'; import { CLINoFlowReport } from './NoFlowReport'; import { IUsageData } from './IUsageData'; @@ -52,8 +52,8 @@ export class NoUsageData implements IUsageData, IFlowData { * @param _input accepted from the CLI */ // eslint-disable-next-line class-methods-use-this - pushInteractiveFlow = (_prompt: string, _input: unknown) : void => { - /* noop */ + pushInteractiveFlow = (_prompt: string, _input: unknown): void => { + /* noop */ } /** @@ -61,23 +61,23 @@ export class NoUsageData implements IUsageData, IFlowData { * @param _headlessFlowDataString accepted from the CLI */ // eslint-disable-next-line class-methods-use-this - pushHeadlessFlow = (_headlessFlowDataString : string) : void => { - /* noop */ + pushHeadlessFlow = (_headlessFlowDataString: string, _input: ICommandInput): void => { + /* noop */ } /** * Noop function to set isHeadless flag in flowLogger * @param _headless */ - setIsHeadless = (_headless: boolean): void => { - /* noop */ + setIsHeadless = (_headless: boolean): void => { + /* noop */ } /** * Empty function is for flow report. * @returns empty object */ - getFlowReport() : IFlowReport | Record { + getFlowReport(): IFlowReport | Record { return {}; } @@ -85,7 +85,7 @@ export class NoUsageData implements IUsageData, IFlowData { * NoOp function to assign Project identifier * @returns undefined */ - assignProjectIdentifier() : string | undefined { + assignProjectIdentifier(): string | undefined { return undefined; } diff --git a/packages/amplify-cli/src/domain/amplify-usageData/UsageData.ts b/packages/amplify-cli/src/domain/amplify-usageData/UsageData.ts index 94382d2357a..cd287401416 100644 --- a/packages/amplify-cli/src/domain/amplify-usageData/UsageData.ts +++ b/packages/amplify-cli/src/domain/amplify-usageData/UsageData.ts @@ -4,7 +4,7 @@ import https from 'https'; import { UrlWithStringQuery } from 'url'; import { $TSAny, JSONUtilities } from 'amplify-cli-core'; import { pick } from 'lodash'; -import { IFlowData, IFlowReport } from 'amplify-cli-shared-interfaces'; +import { ICommandInput, IFlowData, IFlowReport } from 'amplify-cli-shared-interfaces'; import { Input } from '../input'; import redactInput from './identifiable-input-regex'; import { UsageDataPayload, InputOptions } from './UsageDataPayload'; @@ -62,6 +62,7 @@ export class UsageData implements IUsageData, IFlowData { this.codePathTimers.set(FromStartupTimedCodePaths.PLATFORM_STARTUP, Timer.start(processStartTimeStamp)); this.codePathTimers.set(FromStartupTimedCodePaths.TOTAL_DURATION, Timer.start(processStartTimeStamp)); UsageData.flow.setInput(input); + UsageData.flow.setVersion(version); } /** @@ -139,9 +140,9 @@ export class UsageData implements IUsageData, IFlowData { * @param headlessParameterString - Stringified headless parameter string * @param input - CLI input entered by Cx */ - pushHeadlessFlow(headlessParameterString: string) { + pushHeadlessFlow(headlessParameterString: string, input: ICommandInput) { if (UsageData.flow) { - UsageData.flow.pushHeadlessFlow(headlessParameterString); + UsageData.flow.pushHeadlessFlow(headlessParameterString, input); } } @@ -204,10 +205,13 @@ export class UsageData implements IUsageData, IFlowData { Object.fromEntries(this.codePathDurations), UsageData.flowInstance.getFlowReport() as IFlowReport, ); + await this.send(payload); + return payload; } + private async send(payload: UsageDataPayload): Promise { return new Promise(resolve => { const data: string = JSONUtilities.stringify(payload, { diff --git a/packages/amplify-cli/src/index.ts b/packages/amplify-cli/src/index.ts index ab02e7630d1..6cb7db50c6b 100644 --- a/packages/amplify-cli/src/index.ts +++ b/packages/amplify-cli/src/index.ts @@ -36,7 +36,7 @@ import { deleteOldVersion } from './utils/win-utils'; import { notify } from './version-notifier'; import { getAmplifyVersion } from './extensions/amplify-helpers/get-amplify-version'; -export { UsageData } from './domain/amplify-usageData'; +export { UsageData, CLIFlowReport } from './domain/amplify-usageData'; // Adjust defaultMaxListeners to make sure Inquirer will not fail under Windows because of the multiple subscriptions // https://github.com/SBoudrias/Inquirer.js/issues/887 diff --git a/packages/amplify-prompts/src/prompter.ts b/packages/amplify-prompts/src/prompter.ts index 93b0905c60e..0c9dfe81cc1 100644 --- a/packages/amplify-prompts/src/prompter.ts +++ b/packages/amplify-prompts/src/prompter.ts @@ -10,7 +10,9 @@ import { prompt } from 'enquirer'; // @ts-ignore import * as actions from 'enquirer/lib/combos'; import chalk from 'chalk'; -import { IFlowData } from 'amplify-cli-shared-interfaces'; +import { + ICommandInput, IFlowData, +} from 'amplify-cli-shared-interfaces'; import { isYes, isInteractiveShell } from './flags'; import { Validator } from './validators'; import { printer } from './printer'; @@ -38,7 +40,7 @@ class AmplifyPrompter implements Prompter { throw new Error(errorMsg); } - setFlowData = (flowData: IFlowData):void => { + setFlowData = (flowData: IFlowData): void => { this.flowData = flowData; } @@ -48,9 +50,9 @@ class AmplifyPrompter implements Prompter { } } - pushHeadlessFlow = (headlessFlowDataString: string) => { + pushHeadlessFlow = (headlessFlowDataString: string, input: ICommandInput) => { if (this.flowData) { - this.flowData.pushHeadlessFlow(headlessFlowDataString); + this.flowData.pushHeadlessFlow(headlessFlowDataString, input); } } @@ -58,7 +60,7 @@ class AmplifyPrompter implements Prompter { * Asks a continue prompt. * Similar to yesOrNo, but 'false' is always the default and if the --yes flag is set, the prompt is skipped and 'true' is returned */ - confirmContinue = async (message = 'Do you want to continue?') : Promise => { + confirmContinue = async (message = 'Do you want to continue?'): Promise => { let result = false; if (isYes) { result = true; @@ -84,7 +86,7 @@ class AmplifyPrompter implements Prompter { return result; }; - private yesOrNoCommon = async (message: string, initial: boolean):Promise => { + private yesOrNoCommon = async (message: string, initial: boolean): Promise => { let submitted = false; const { result } = await this.prompter<{ result: boolean }>({ type: 'confirm', @@ -347,7 +349,7 @@ type Prompter = { // options is typed using spread because it's the only way to make it required if RS is 'many' but optional if RS is 'one' ...options: MaybeOptionalPickOptions ) => Promise>; - setFlowData : (flowData: IFlowData)=>void; + setFlowData: (flowData: IFlowData) => void; }; // the following types are the building blocks of the method input types @@ -356,8 +358,8 @@ type Prompter = { type MaybeAvailableHiddenInputOption = RS extends 'many' ? unknown : { - hidden?: boolean; - }; + hidden?: boolean; + }; // The initial selection for a pick prompt can be specified either by index or a selection function that generates indexes. // See byValues and byValue above @@ -376,14 +378,14 @@ type InitialValueOption = { type MultiSelectMinimum = RS extends 'one' ? unknown : { - pickAtLeast?: number; - }; + pickAtLeast?: number; + }; type MultiSelectMaximum = RS extends 'one' ? unknown : { - pickAtMost?: number; - }; + pickAtMost?: number; + }; type ValidateValueOption = { validate?: Validator; @@ -399,11 +401,11 @@ type MaybeOptionalTransformOption = T extends string ? Partial = RS extends 'many' ? { - returnSize: 'many'; - } + returnSize: 'many'; + } : { - returnSize?: 'one'; - }; + returnSize?: 'one'; + }; type Choices = T extends string ? GenericChoice[] | string[] : GenericChoice[];