diff --git a/core/src/analytics/analytics.ts b/core/src/analytics/analytics.ts index 43217ef9d22..9435ee31c9c 100644 --- a/core/src/analytics/analytics.ts +++ b/core/src/analytics/analytics.ts @@ -132,6 +132,7 @@ interface CommandEvent extends EventBase { type: "Run Command" properties: PropertiesBase & { name: string + commandIteration: number } } @@ -152,6 +153,7 @@ interface CommandResultEvent extends EventBase { result: AnalyticsCommandResult errors: string[] // list of GardenBaseError types exitCode?: number + commandIteration: number } } @@ -578,11 +580,12 @@ export class AnalyticsHandler { /** * Tracks a Command. */ - trackCommand(commandName: string) { + trackCommand({ commandName, commandIteration = 1 }: { commandName: string; commandIteration?: number }) { return this.track({ type: "Run Command", properties: { name: commandName, + commandIteration, ...this.getBasicAnalyticsProperties(), }, }) @@ -591,7 +594,13 @@ export class AnalyticsHandler { /** * Track a command result. */ - trackCommandResult(commandName: string, errors: GardenBaseError[], startTime: Date, exitCode?: number) { + trackCommandResult( + commandName: string, + commandIteration: number, + errors: GardenBaseError[], + startTime: Date, + exitCode?: number + ) { const result: AnalyticsCommandResult = errors.length > 0 ? "failure" : "success" const durationMsec = getDurationMsec(startTime, new Date()) @@ -600,6 +609,7 @@ export class AnalyticsHandler { type: "Command Result", properties: { name: commandName, + commandIteration, durationMsec, result, errors: errors.map((e) => e.type), diff --git a/core/src/cli/cli.ts b/core/src/cli/cli.ts index 09c21f5242d..acf51e76b5a 100644 --- a/core/src/cli/cli.ts +++ b/core/src/cli/cli.ts @@ -361,7 +361,11 @@ ${renderCommands(commands)} // TODO: Link to Cloud namespace page here. const nsLog = log.createLog({ name: "garden" }) + let commandIteration = 0 + do { + commandIteration += 1 + try { if (command.noProject) { garden = await makeDummyGarden(workingDir, contextOpts) @@ -462,7 +466,7 @@ ${renderCommands(commands)} commandFullName: command.getFullName(), }) analytics = await AnalyticsHandler.init(garden, log) - analytics.trackCommand(command.getFullName()) + analytics.trackCommand({ commandName: command.getFullName(), commandIteration }) // Note: No reason to await the check checkForUpdates(garden.globalConfigStore, log).catch((err) => { @@ -472,6 +476,8 @@ ${renderCommands(commands)} await checkForStaticDir() + const commandStartTime = new Date() + // Check if the command is protected and ask for confirmation to proceed if production flag is "true". if (await command.isAllowedToRun(garden, log, parsedOpts)) { // TODO: enforce that commands always output DeepPrimitiveMap @@ -491,6 +497,16 @@ ${renderCommands(commands)} result = {} } + // Track the result of the command run + const allErrors = result.errors || [] + analytics?.trackCommandResult( + command.getFullName(), + commandIteration, + allErrors, + commandStartTime, + result.exitCode + ) + // This is a little trick to do a round trip in the event loop, which may be necessary for event handlers to // fire, which may be needed to e.g. capture monitors added in event handlers await waitForOutputFlush() @@ -664,8 +680,6 @@ ${renderCommands(commands)} this.processRecord = processRecord! - const commandStartTime = new Date() - try { const runResults = await this.runCommand({ command, parsedArgs, parsedOpts, processRecord, workingDir, log }) commandResult = runResults.result @@ -678,8 +692,6 @@ ${renderCommands(commands)} const gardenErrors: GardenBaseError[] = errors.map(toGardenError) - analytics?.trackCommandResult(command.getFullName(), gardenErrors, commandStartTime, commandResult.exitCode) - // Flushes the Analytics events queue in case there are some remaining events. if (analytics) { await analytics.flush() diff --git a/core/test/unit/src/analytics/analytics.ts b/core/test/unit/src/analytics/analytics.ts index 092db6c60cc..c1cac26e6f8 100644 --- a/core/test/unit/src/analytics/analytics.ts +++ b/core/test/unit/src/analytics/analytics.ts @@ -394,12 +394,13 @@ describe("AnalyticsHandler", () => { await garden.globalConfigStore.set("analytics", basicConfig) const now = freezeTime() analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo }) - const event = analytics.trackCommand("testCommand") + const event = analytics.trackCommand({ commandName: "testCommand" }) expect(event).to.eql({ type: "Run Command", properties: { name: "testCommand", + commandIteration: 1, projectId: AnalyticsHandler.hash(remoteOriginUrl), projectIdV2: AnalyticsHandler.hashV2(remoteOriginUrl), projectName, @@ -438,12 +439,13 @@ describe("AnalyticsHandler", () => { await garden.globalConfigStore.set("analytics", basicConfig) const now = freezeTime() analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo: { isCi: true, ciName: "foo" } }) - const event = analytics.trackCommand("testCommand") + const event = analytics.trackCommand({ commandName: "testCommand" }) expect(event).to.eql({ type: "Run Command", properties: { name: "testCommand", + commandIteration: 1, projectId: AnalyticsHandler.hash(remoteOriginUrl), projectIdV2: AnalyticsHandler.hashV2(remoteOriginUrl), projectName, @@ -499,12 +501,13 @@ describe("AnalyticsHandler", () => { const now = freezeTime() analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo }) - const event = analytics.trackCommand("testCommand") + const event = analytics.trackCommand({ commandName: "testCommand" }) expect(event).to.eql({ type: "Run Command", properties: { name: "testCommand", + commandIteration: 1, projectId: AnalyticsHandler.hash(remoteOriginUrl), projectIdV2: AnalyticsHandler.hashV2(remoteOriginUrl), projectName, @@ -548,12 +551,13 @@ describe("AnalyticsHandler", () => { const now = freezeTime() analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo }) - const event = analytics.trackCommand("testCommand") + const event = analytics.trackCommand({ commandName: "testCommand" }) expect(event).to.eql({ type: "Run Command", properties: { name: "testCommand", + commandIteration: 1, projectId: AnalyticsHandler.hash(remoteOriginUrl), projectIdV2: AnalyticsHandler.hashV2(remoteOriginUrl), projectName: AnalyticsHandler.hash("has-domain-and-id"), @@ -597,12 +601,13 @@ describe("AnalyticsHandler", () => { const now = freezeTime() analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo }) - const event = analytics.trackCommand("testCommand") + const event = analytics.trackCommand({ commandName: "testCommand" }) expect(event).to.eql({ type: "Run Command", properties: { name: "testCommand", + commandIteration: 1, projectId: AnalyticsHandler.hash(remoteOriginUrl), projectIdV2: AnalyticsHandler.hashV2(remoteOriginUrl), projectName: AnalyticsHandler.hash("config-templates"), @@ -677,7 +682,7 @@ describe("AnalyticsHandler", () => { await garden.globalConfigStore.set("analytics", basicConfig) analytics = await AnalyticsHandler.factory({ garden, log: garden.log, ciInfo }) - analytics.trackCommand("test-command-A") + analytics.trackCommand({ commandName: "test-command-A" }) await analytics.flush() expect(analytics["pendingEvents"].size).to.eql(0)