diff --git a/garden-service/src/analytics/analytics.ts b/garden-service/src/analytics/analytics.ts index 5d6e1607b8..7d894f8959 100644 --- a/garden-service/src/analytics/analytics.ts +++ b/garden-service/src/analytics/analytics.ts @@ -10,7 +10,7 @@ import uuidv4 from "uuid/v4" import segmentClient = require("analytics-node") import { platform, release } from "os" import ci = require("ci-info") - +import { flatten } from "lodash" import { globalConfigKeys, AnalyticsGlobalConfig, GlobalConfigStore } from "../config-store" import { getPackageVersion } from "../util/util" import { SEGMENT_PROD_API_KEY, SEGMENT_DEV_API_KEY } from "../constants" @@ -22,7 +22,6 @@ import uuid from "uuid" import { Garden } from "../garden" import { Events, EventName } from "../events" import { AnalyticsType } from "./analytics-types" -import { TestConfig } from "../config/test" import dedent from "dedent" const API_KEY = process.env.ANALYTICS_DEV ? SEGMENT_DEV_API_KEY : SEGMENT_PROD_API_KEY @@ -111,7 +110,7 @@ export class AnalyticsHandler { private systemConfig: SystemInfo private isCI = ci.isCI private sessionId = uuid.v4() - private garden: Garden + protected garden: Garden private projectMetadata private constructor(garden: Garden, log: LogEntry) { @@ -135,7 +134,14 @@ export class AnalyticsHandler { static async init(garden: Garden, log: LogEntry) { if (!AnalyticsHandler.instance) { - AnalyticsHandler.instance = await new AnalyticsHandler(garden, log).initialize() + AnalyticsHandler.instance = await new AnalyticsHandler(garden, log).factory() + } else { + /** + * This init is called from within the do while loop in the cli + * If the instance is already present it means a restart happened and we need to + * refresh the garden instance and event listeners. + */ + await AnalyticsHandler.refreshGarden(garden) } return AnalyticsHandler.instance } @@ -149,14 +155,14 @@ export class AnalyticsHandler { /** * A private initialization function which returns an initialized Analytics object, ready to be used. - * This function will load global update it if needed. + * This function will load the globalConfigStore and update it if needed. * The globalConfigStore contains info about optIn, first run, machine info, etc., * This method always needs to be called after instantiation. * * @returns * @memberof AnalyticsHandler */ - private async initialize() { + private async factory() { const globalConf = await this.globalConfigStore.get() this.globalConfig = { ...this.globalConfig, @@ -164,8 +170,8 @@ export class AnalyticsHandler { } const vcs = new GitHandler(process.cwd(), []) - const originName = await vcs.getOriginName() - this.projectId = originName ? hasha(await vcs.getOriginName(), { algorithm: "sha256" }) : "unset" + const originName = await vcs.getOriginName(this.log) + this.projectId = originName ? hasha(originName, { algorithm: "sha256" }) : "unset" if (this.globalConfig.firstRun || this.globalConfig.showOptInMessage) { if (!this.isCI) { @@ -174,7 +180,7 @@ export class AnalyticsHandler { dedent` Thanks for installing Garden! We work hard to provide you with the best experience we can. We collect some anonymized usage data while you use Garden. If you'd like to know more about what we collect - or you'd like to to opt-out, please read more at https://github.com/garden-io/garden/blob/master/README.md#Analytics` + or if you'd like to opt out of telemetry, please read more at https://github.com/garden-io/garden/blob/master/README.md#Analytics` ) } @@ -215,6 +221,13 @@ export class AnalyticsHandler { } } + static async refreshGarden(garden: Garden) { + AnalyticsHandler.instance.garden = garden + AnalyticsHandler.instance.garden.events.onAny((name, payload) => + AnalyticsHandler.instance.processEvent(name, payload) + ) + } + /** * Typeguard to check wether we can process or not an event */ @@ -238,16 +251,16 @@ export class AnalyticsHandler { private async generateProjectMetadata() { const configGraph = await this.garden.getConfigGraph(this.log) const modules = await configGraph.getModules() - const modulesTypes = [...new Set(modules.map((m) => m.type))] + const moduleTypes = [...new Set(modules.map((m) => m.type))] const tasks = await configGraph.getTasks() const services = await configGraph.getServices() const tests = modules.map((m) => m.testConfigs) - const numberOfTests = ([] as TestConfig[]).concat(...tests).length + const numberOfTests = flatten(tests).length return { numberOfModules: modules.length, - modulesTypes, + moduleTypes, numberOfTasks: tasks.length, numberOfServices: services.length, numberOfTests, @@ -385,11 +398,13 @@ export class AnalyticsHandler { * @returns * @memberof AnalyticsHandler */ - trackModuleConfigError(moduleType: string) { + trackModuleConfigError(name: string, moduleType: string) { + const moduleName = hasha(name, { algorithm: "sha256" }) return this.track({ type: AnalyticsType.MODULE_CONFIG_ERROR, properties: { ...this.getBasicAnalyticsProperties(), + moduleName, moduleType, }, }) diff --git a/garden-service/src/task-graph.ts b/garden-service/src/task-graph.ts index 545b190028..a40cba57fd 100644 --- a/garden-service/src/task-graph.ts +++ b/garden-service/src/task-graph.ts @@ -78,8 +78,6 @@ export class TaskGraph { private resultCache: ResultCache private opQueue: PQueue - private batchId: string - constructor(private garden: Garden, private log: LogEntry) { this.roots = new TaskNodeMap() this.index = new TaskNodeMap() @@ -90,12 +88,11 @@ export class TaskGraph { this.resultCache = new ResultCache() this.opQueue = new PQueue({ concurrency: 1 }) this.logEntryMap = {} - this.batchId = uuid.v4() } async process(tasks: BaseTask[], opts?: ProcessTasksOpts): Promise { // We generate a new batchId - this.batchId = uuid.v4() + const batchId = uuid.v4() for (const t of tasks) { this.latestTasks[t.getKey()] = t @@ -112,7 +109,7 @@ export class TaskGraph { // to return the latest result for each requested task. const resultKeys = tasks.map((t) => t.getKey()) - const results = await this.opQueue.add(() => this.processTasksInternal(tasksToProcess, resultKeys, opts)) + const results = await this.opQueue.add(() => this.processTasksInternal(batchId, tasksToProcess, resultKeys, opts)) if (opts && opts.throwOnError) { const failed = Object.entries(results).filter(([_, result]) => result && result.error) @@ -153,13 +150,13 @@ export class TaskGraph { this.roots.setNodes(newRootNodes) } - private async addTask(task: BaseTask) { + private async addTask(batchId: string, task: BaseTask) { await this.addNodeWithDependencies(task) this.rebuild() if (this.index.getNode(task)) { this.garden.events.emit("taskPending", { addedAt: new Date(), - batchId: this.batchId, + batchId, key: task.getKey(), name: task.getName(), type: task.type, @@ -198,6 +195,7 @@ export class TaskGraph { * Process the graph until it's complete. */ private async processTasksInternal( + batchId: string, tasks: BaseTask[], resultKeys: string[], opts?: ProcessTasksOpts @@ -205,7 +203,7 @@ export class TaskGraph { const { concurrencyLimit = defaultTaskConcurrency } = opts || {} for (const task of tasks) { - await this.addTask(this.latestTasks[task.getKey()]) + await this.addTask(batchId, this.latestTasks[task.getKey()]) } this.log.silly("") @@ -261,18 +259,18 @@ export class TaskGraph { type, key, startedAt: new Date(), - batchId: this.batchId, + batchId, version: task.version, }) - result = await node.process(dependencyResults, this.batchId) + result = await node.process(dependencyResults, batchId) this.garden.events.emit("taskComplete", result) } catch (error) { success = false - result = { type, description, key, name, error, completedAt: new Date(), batchId: this.batchId } + result = { type, description, key, name, error, completedAt: new Date(), batchId } this.garden.events.emit("taskError", result) this.logTaskError(node, error) - this.cancelDependants(node) + this.cancelDependants(batchId, node) } finally { // We know the result got assigned in either the try or catch clause results[key] = result! @@ -339,7 +337,7 @@ export class TaskGraph { } // Recursively remove node's dependants, without removing node. - private cancelDependants(node: TaskNode) { + private cancelDependants(batchId: string, node: TaskNode) { const cancelledAt = new Date() for (const dependant of this.getDependants(node)) { this.logTaskComplete(dependant, false) @@ -348,7 +346,7 @@ export class TaskGraph { key: dependant.getKey(), name: dependant.task.getName(), type: dependant.getType(), - batchId: this.batchId, + batchId, }) this.remove(dependant) } @@ -419,10 +417,6 @@ export class TaskGraph { this.log.error({ msg, error }) } - - getBatchId() { - return this.batchId - } } function getIndexId(task: BaseTask) { diff --git a/garden-service/src/vcs/git.ts b/garden-service/src/vcs/git.ts index 90a09b62dc..2b2db00245 100644 --- a/garden-service/src/vcs/git.ts +++ b/garden-service/src/vcs/git.ts @@ -21,7 +21,6 @@ import { deline } from "../util/string" import { splitLast, exec } from "../util/util" import { LogEntry } from "../logger/log-entry" import parseGitConfig from "parse-git-config" -import { Logger } from "../logger/logger" export function getCommitIdFromRefList(refList: string[]): string { try { @@ -387,11 +386,9 @@ export class GitHandler extends VcsHandler { return submodules } - async getOriginName() { + async getOriginName(log: LogEntry) { const cwd = process.cwd() - const log = Logger.getInstance().placeholder() const git = this.gitCli(log, cwd) - return (await git("config", "--get", "remote.origin.url"))[0] } } diff --git a/garden-service/test/unit/src/task-graph.ts b/garden-service/test/unit/src/task-graph.ts index 5dbc4029ab..a187e45806 100644 --- a/garden-service/test/unit/src/task-graph.ts +++ b/garden-service/test/unit/src/task-graph.ts @@ -6,6 +6,7 @@ import { TaskGraph, TaskResult, TaskResults } from "../../../src/task-graph" import { makeTestGarden, freezeTime, dataDir, defer } from "../../helpers" import { Garden } from "../../../src/garden" import { deepFilter } from "../../../src/util/util" +import uuid from "uuid" const projectRoot = join(dataDir, "test-project-empty") @@ -93,6 +94,7 @@ describe("task-graph", () => { const task = new TestTask(garden, "a", false) const results = await graph.process([task]) + const generatedBatchId = results?.a?.batchId || uuid.v4() const expected: TaskResults = { a: { @@ -101,7 +103,7 @@ describe("task-graph", () => { key: "a", name: "a", completedAt: now, - batchId: graph.getBatchId(), + batchId: generatedBatchId, output: { result: "result-a", dependencyResults: {}, @@ -121,14 +123,14 @@ describe("task-graph", () => { const task = new TestTask(garden, "a", false) const result = await graph.process([task]) - const batchId = graph.getBatchId() + const generatedBatchId = result?.a?.batchId || uuid.v4() expect(garden.events.eventLog).to.eql([ { name: "taskPending", payload: { addedAt: now, - batchId, + batchId: generatedBatchId, key: task.getKey(), name: task.name, type: task.type, @@ -139,7 +141,7 @@ describe("task-graph", () => { name: "taskProcessing", payload: { startedAt: now, - batchId, + batchId: generatedBatchId, key: task.getKey(), name: task.name, type: task.type, @@ -162,13 +164,13 @@ describe("task-graph", () => { const graph = new TaskGraph(garden, garden.log) const task = new TestTask(garden, "a", false) await graph.process([task]) - const batchId = graph.getBatchId() garden.events.eventLog = [] // repeatedTask has the same key and version as task, so its result is already cached const repeatedTask = new TestTask(garden, "a", false) - await graph.process([repeatedTask]) + const results = await graph.process([repeatedTask]) + const generatedBatchId = results?.a?.batchId || uuid.v4() expect(garden.events.eventLog).to.eql([ { @@ -176,7 +178,7 @@ describe("task-graph", () => { payload: { completedAt: now, dependencyResults: {}, - batchId, + batchId: generatedBatchId, description: "a", key: task.getKey(), type: "test", @@ -197,14 +199,14 @@ describe("task-graph", () => { const task = new TestTask(garden, "a", false, { throwError: true }) const result = await graph.process([task]) - const batchId = graph.getBatchId() + const generatedBatchId = result?.a?.batchId || uuid.v4() expect(garden.events.eventLog).to.eql([ { name: "taskPending", payload: { addedAt: now, - batchId, + batchId: generatedBatchId, key: task.getKey(), name: task.name, type: task.type, @@ -215,7 +217,7 @@ describe("task-graph", () => { name: "taskProcessing", payload: { startedAt: now, - batchId, + batchId: generatedBatchId, key: task.getKey(), name: task.name, type: task.type, @@ -262,7 +264,7 @@ describe("task-graph", () => { // we should be able to add tasks multiple times and in any order const results = await graph.process([taskA, taskB, taskC, taskC, taskD, taskA, taskD, taskB, taskD, taskA]) - const updatedBatchId = graph.getBatchId() + const generatedBatchId = results?.a?.batchId || uuid.v4() // repeat @@ -295,7 +297,7 @@ describe("task-graph", () => { key: "a", name: "a", completedAt: now, - batchId: updatedBatchId, + batchId: generatedBatchId, output: { result: "result-a.a1", dependencyResults: {}, @@ -308,7 +310,7 @@ describe("task-graph", () => { name: "b", description: "b.b1", completedAt: now, - batchId: updatedBatchId, + batchId: generatedBatchId, output: { result: "result-b.b1", dependencyResults: { a: resultA }, @@ -321,7 +323,7 @@ describe("task-graph", () => { key: "c", name: "c", completedAt: now, - batchId: updatedBatchId, + batchId: generatedBatchId, output: { result: "result-c.c1", dependencyResults: { b: resultB }, @@ -339,7 +341,7 @@ describe("task-graph", () => { key: "d", name: "d", completedAt: now, - batchId: updatedBatchId, + batchId: generatedBatchId, output: { result: "result-d.d1", dependencyResults: { @@ -437,7 +439,8 @@ describe("task-graph", () => { const taskD = new TestTask(garden, "d", true, { ...opts, dependencies: [taskB, taskC] }) const results = await graph.process([taskA, taskB, taskC, taskD]) - const batchId = graph.getBatchId() + + const generatedBatchId = results?.a?.batchId || uuid.v4() const resultA: TaskResult = { type: "test", @@ -445,7 +448,7 @@ describe("task-graph", () => { key: "a", name: "a", completedAt: now, - batchId, + batchId: generatedBatchId, output: { result: "result-a", dependencyResults: {}, @@ -470,12 +473,12 @@ describe("task-graph", () => { expect(results.b).to.have.property("error") expect(resultOrder).to.eql(["a", "b"]) expect(filteredEventLog).to.eql([ - { name: "taskPending", payload: { key: "a", name: "a", type: "test", batchId } }, - { name: "taskPending", payload: { key: "b", name: "b", type: "test", batchId } }, - { name: "taskPending", payload: { key: "c", name: "c", type: "test", batchId } }, - { name: "taskPending", payload: { key: "d", name: "d", type: "test", batchId } }, + { name: "taskPending", payload: { key: "a", name: "a", type: "test", batchId: generatedBatchId } }, + { name: "taskPending", payload: { key: "b", name: "b", type: "test", batchId: generatedBatchId } }, + { name: "taskPending", payload: { key: "c", name: "c", type: "test", batchId: generatedBatchId } }, + { name: "taskPending", payload: { key: "d", name: "d", type: "test", batchId: generatedBatchId } }, { name: "taskGraphProcessing", payload: {} }, - { name: "taskProcessing", payload: { key: "a", name: "a", type: "test", batchId } }, + { name: "taskProcessing", payload: { key: "a", name: "a", type: "test", batchId: generatedBatchId } }, { name: "taskComplete", payload: { @@ -485,14 +488,17 @@ describe("task-graph", () => { name: "a", output: { dependencyResults: {}, result: "result-a" }, type: "test", - batchId, + batchId: generatedBatchId, }, }, - { name: "taskProcessing", payload: { key: "b", name: "b", type: "test", batchId } }, - { name: "taskError", payload: { description: "b", key: "b", name: "b", type: "test", batchId } }, - { name: "taskCancelled", payload: { key: "c", name: "c", type: "test", batchId } }, - { name: "taskCancelled", payload: { key: "d", name: "d", type: "test", batchId } }, - { name: "taskCancelled", payload: { key: "d", name: "d", type: "test", batchId } }, + { name: "taskProcessing", payload: { key: "b", name: "b", type: "test", batchId: generatedBatchId } }, + { + name: "taskError", + payload: { description: "b", key: "b", name: "b", type: "test", batchId: generatedBatchId }, + }, + { name: "taskCancelled", payload: { key: "c", name: "c", type: "test", batchId: generatedBatchId } }, + { name: "taskCancelled", payload: { key: "d", name: "d", type: "test", batchId: generatedBatchId } }, + { name: "taskCancelled", payload: { key: "d", name: "d", type: "test", batchId: generatedBatchId } }, { name: "taskGraphComplete", payload: {} }, ]) })