From 2125f2579157ba261d78f4af5bc7c96e0917262c Mon Sep 17 00:00:00 2001 From: Ahsan Virani Date: Fri, 10 Dec 2021 15:29:05 +0100 Subject: [PATCH] :zap: Tweaks to diagnostic events (#2544) * Tweaks to events * more tweaks and fixes --- packages/cli/commands/execute.ts | 4 +- packages/cli/commands/executeBatch.ts | 4 +- packages/cli/commands/start.ts | 14 +-- packages/cli/commands/webhook.ts | 3 +- packages/cli/commands/worker.ts | 6 +- packages/cli/src/Interfaces.ts | 5 +- packages/cli/src/InternalHooks.ts | 29 ++++-- packages/cli/src/InternalHooksManager.ts | 7 +- packages/cli/src/Server.ts | 9 +- packages/cli/src/WorkflowRunnerProcess.ts | 4 +- packages/cli/src/telemetry/index.ts | 90 ++++++++++++++----- packages/editor-ui/src/App.vue | 10 ++- .../editor-ui/src/plugins/telemetry/index.ts | 6 ++ packages/editor-ui/src/views/NodeView.vue | 1 + 14 files changed, 142 insertions(+), 50 deletions(-) diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index b74eb7397d..c9eefd9145 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -11,6 +11,7 @@ import { CredentialTypes, Db, ExternalHooks, + GenericHelpers, InternalHooksManager, IWorkflowBase, IWorkflowExecutionDataProcess, @@ -125,7 +126,8 @@ export class Execute extends Command { await externalHooks.init(); const instanceId = await UserSettings.getInstanceId(); - InternalHooksManager.init(instanceId); + const { cli } = await GenericHelpers.getVersions(); + InternalHooksManager.init(instanceId, cli); // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); diff --git a/packages/cli/commands/executeBatch.ts b/packages/cli/commands/executeBatch.ts index f415e3c5c2..69827f468c 100644 --- a/packages/cli/commands/executeBatch.ts +++ b/packages/cli/commands/executeBatch.ts @@ -28,6 +28,7 @@ import { CredentialTypes, Db, ExternalHooks, + GenericHelpers, InternalHooksManager, IWorkflowDb, IWorkflowExecutionDataProcess, @@ -305,7 +306,8 @@ export class ExecuteBatch extends Command { await externalHooks.init(); const instanceId = await UserSettings.getInstanceId(); - InternalHooksManager.init(instanceId); + const { cli } = await GenericHelpers.getVersions(); + InternalHooksManager.init(instanceId, cli); // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index 253877e564..e34b457810 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -153,17 +153,6 @@ export class Start extends Command { LoggerProxy.init(logger); logger.info('Initializing n8n process'); - logger.info( - '\n' + - '****************************************************\n' + - '* *\n' + - '* n8n now sends selected, anonymous telemetry. *\n' + - '* For more details (and how to opt out): *\n' + - '* https://docs.n8n.io/reference/telemetry.html *\n' + - '* *\n' + - '****************************************************\n', - ); - // Start directly with the init of the database to improve startup time const startDbInitPromise = Db.init().catch((error: Error) => { logger.error(`There was an error initializing DB: "${error.message}"`); @@ -313,7 +302,8 @@ export class Start extends Command { } const instanceId = await UserSettings.getInstanceId(); - InternalHooksManager.init(instanceId); + const { cli } = await GenericHelpers.getVersions(); + InternalHooksManager.init(instanceId, cli); await Server.start(); diff --git a/packages/cli/commands/webhook.ts b/packages/cli/commands/webhook.ts index a5f926682f..ebf683e6ca 100644 --- a/packages/cli/commands/webhook.ts +++ b/packages/cli/commands/webhook.ts @@ -149,7 +149,8 @@ export class Webhook extends Command { await startDbInitPromise; const instanceId = await UserSettings.getInstanceId(); - InternalHooksManager.init(instanceId); + const { cli } = await GenericHelpers.getVersions(); + InternalHooksManager.init(instanceId, cli); if (config.get('executions.mode') === 'queue') { const redisHost = config.get('queue.bull.redis.host'); diff --git a/packages/cli/commands/worker.ts b/packages/cli/commands/worker.ts index 28a02b9b39..1290868abf 100644 --- a/packages/cli/commands/worker.ts +++ b/packages/cli/commands/worker.ts @@ -271,10 +271,10 @@ export class Worker extends Command { // eslint-disable-next-line @typescript-eslint/no-floating-promises Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes)); - const instanceId = await UserSettings.getInstanceId(); - InternalHooksManager.init(instanceId); - const versions = await GenericHelpers.getVersions(); + const instanceId = await UserSettings.getInstanceId(); + + InternalHooksManager.init(instanceId, versions.cli); console.info('\nn8n worker is now ready'); console.info(` * Version: ${versions.cli}`); diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index 556aa74492..30cd03b8b0 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -314,7 +314,10 @@ export interface IDiagnosticInfo { export interface IInternalHooksClass { onN8nStop(): Promise; - onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise; + onServerStarted( + diagnosticInfo: IDiagnosticInfo, + firstWorkflowCreatedAt?: Date, + ): Promise; onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise; onWorkflowCreated(workflow: IWorkflowBase): Promise; onWorkflowDeleted(workflowId: string): Promise; diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 4f6d4839d6..fc67a3634e 100644 --- a/packages/cli/src/InternalHooks.ts +++ b/packages/cli/src/InternalHooks.ts @@ -9,9 +9,16 @@ import { import { Telemetry } from './telemetry'; export class InternalHooksClass implements IInternalHooksClass { - constructor(private telemetry: Telemetry) {} + private versionCli: string; - async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise { + constructor(private telemetry: Telemetry, versionCli: string) { + this.versionCli = versionCli; + } + + async onServerStarted( + diagnosticInfo: IDiagnosticInfo, + earliestWorkflowCreatedAt?: Date, + ): Promise { const info = { version_cli: diagnosticInfo.versionCli, db_type: diagnosticInfo.databaseType, @@ -25,7 +32,10 @@ export class InternalHooksClass implements IInternalHooksClass { return Promise.all([ this.telemetry.identify(info), - this.telemetry.track('Instance started', info), + this.telemetry.track('Instance started', { + ...info, + earliest_workflow_created: earliestWorkflowCreatedAt, + }), ]); } @@ -39,9 +49,11 @@ export class InternalHooksClass implements IInternalHooksClass { } async onWorkflowCreated(workflow: IWorkflowBase): Promise { + const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow); return this.telemetry.track('User created workflow', { workflow_id: workflow.id, - node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph, + node_graph: nodeGraph, + node_graph_string: JSON.stringify(nodeGraph), }); } @@ -52,9 +64,13 @@ export class InternalHooksClass implements IInternalHooksClass { } async onWorkflowSaved(workflow: IWorkflowBase): Promise { + const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow); + return this.telemetry.track('User saved workflow', { workflow_id: workflow.id, - node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph, + node_graph: nodeGraph, + node_graph_string: JSON.stringify(nodeGraph), + version_cli: this.versionCli, }); } @@ -62,6 +78,7 @@ export class InternalHooksClass implements IInternalHooksClass { const properties: IDataObject = { workflow_id: workflow.id, is_manual: false, + version_cli: this.versionCli, }; if (runData !== undefined) { @@ -92,6 +109,8 @@ export class InternalHooksClass implements IInternalHooksClass { if (properties.is_manual) { const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow); properties.node_graph = nodeGraphResult.nodeGraph; + properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph); + if (errorNodeName) { properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName]; } diff --git a/packages/cli/src/InternalHooksManager.ts b/packages/cli/src/InternalHooksManager.ts index 28087b3702..d050cb04c8 100644 --- a/packages/cli/src/InternalHooksManager.ts +++ b/packages/cli/src/InternalHooksManager.ts @@ -13,9 +13,12 @@ export class InternalHooksManager { throw new Error('InternalHooks not initialized'); } - static init(instanceId: string): InternalHooksClass { + static init(instanceId: string, versionCli: string): InternalHooksClass { if (!this.internalHooksInstance) { - this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId)); + this.internalHooksInstance = new InternalHooksClass( + new Telemetry(instanceId, versionCli), + versionCli, + ); } return this.internalHooksInstance; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 6ea877f54e..c636db56f0 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -2896,7 +2896,14 @@ export async function start(): Promise { deploymentType: config.get('deployment.type'), }; - void InternalHooksManager.getInstance().onServerStarted(diagnosticInfo); + void Db.collections + .Workflow!.findOne({ + select: ['createdAt'], + order: { createdAt: 'ASC' }, + }) + .then(async (workflow) => + InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt), + ); }); } diff --git a/packages/cli/src/WorkflowRunnerProcess.ts b/packages/cli/src/WorkflowRunnerProcess.ts index e8b8274c9f..862fa4303f 100644 --- a/packages/cli/src/WorkflowRunnerProcess.ts +++ b/packages/cli/src/WorkflowRunnerProcess.ts @@ -31,6 +31,7 @@ import { CredentialTypes, Db, ExternalHooks, + GenericHelpers, IWorkflowExecuteProcess, IWorkflowExecutionDataProcessWithExecution, NodeTypes, @@ -137,7 +138,8 @@ export class WorkflowRunnerProcess { await externalHooks.init(); const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? ''; - InternalHooksManager.init(instanceId); + const { cli } = await GenericHelpers.getVersions(); + InternalHooksManager.init(instanceId, cli); // Credentials should now be loaded from database. // We check if any node uses credentials. If it does, then diff --git a/packages/cli/src/telemetry/index.ts b/packages/cli/src/telemetry/index.ts index d350c6d8bd..fb4f53460d 100644 --- a/packages/cli/src/telemetry/index.ts +++ b/packages/cli/src/telemetry/index.ts @@ -5,28 +5,57 @@ import { IDataObject, LoggerProxy } from 'n8n-workflow'; import config = require('../../config'); import { getLogger } from '../Logger'; -interface IExecutionCountsBufferItem { - manual_success_count: number; - manual_error_count: number; - prod_success_count: number; - prod_error_count: number; -} +type CountBufferItemKey = + | 'manual_success_count' + | 'manual_error_count' + | 'prod_success_count' + | 'prod_error_count'; + +type FirstExecutionItemKey = + | 'first_manual_success' + | 'first_manual_error' + | 'first_prod_success' + | 'first_prod_error'; + +type IExecutionCountsBufferItem = { + [key in CountBufferItemKey]: number; +}; interface IExecutionCountsBuffer { [workflowId: string]: IExecutionCountsBufferItem; } +type IFirstExecutions = { + [key in FirstExecutionItemKey]: Date | undefined; +}; + +interface IExecutionsBuffer { + counts: IExecutionCountsBuffer; + firstExecutions: IFirstExecutions; +} + export class Telemetry { private client?: TelemetryClient; private instanceId: string; + private versionCli: string; + private pulseIntervalReference: NodeJS.Timeout; - private executionCountsBuffer: IExecutionCountsBuffer = {}; + private executionCountsBuffer: IExecutionsBuffer = { + counts: {}, + firstExecutions: { + first_manual_error: undefined, + first_manual_success: undefined, + first_prod_error: undefined, + first_prod_success: undefined, + }, + }; - constructor(instanceId: string) { + constructor(instanceId: string, versionCli: string) { this.instanceId = instanceId; + this.versionCli = versionCli; const enabled = config.get('diagnostics.enabled') as boolean; if (enabled) { @@ -53,33 +82,41 @@ export class Telemetry { return Promise.resolve(); } - const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => { + const allPromises = Object.keys(this.executionCountsBuffer.counts).map(async (workflowId) => { const promise = this.track('Workflow execution count', { + version_cli: this.versionCli, workflow_id: workflowId, - ...this.executionCountsBuffer[workflowId], + ...this.executionCountsBuffer.counts[workflowId], + ...this.executionCountsBuffer.firstExecutions, }); - this.executionCountsBuffer[workflowId].manual_error_count = 0; - this.executionCountsBuffer[workflowId].manual_success_count = 0; - this.executionCountsBuffer[workflowId].prod_error_count = 0; - this.executionCountsBuffer[workflowId].prod_success_count = 0; + + this.executionCountsBuffer.counts[workflowId].manual_error_count = 0; + this.executionCountsBuffer.counts[workflowId].manual_success_count = 0; + this.executionCountsBuffer.counts[workflowId].prod_error_count = 0; + this.executionCountsBuffer.counts[workflowId].prod_success_count = 0; return promise; }); - allPromises.push(this.track('pulse')); + allPromises.push(this.track('pulse', { version_cli: this.versionCli })); return Promise.all(allPromises); } async trackWorkflowExecution(properties: IDataObject): Promise { if (this.client) { const workflowId = properties.workflow_id as string; - this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? { + this.executionCountsBuffer.counts[workflowId] = this.executionCountsBuffer.counts[ + workflowId + ] ?? { manual_error_count: 0, manual_success_count: 0, prod_error_count: 0, prod_success_count: 0, }; + let countKey: CountBufferItemKey; + let firstExecKey: FirstExecutionItemKey; + if ( properties.success === false && properties.error_node_type && @@ -89,15 +126,28 @@ export class Telemetry { void this.track('Workflow execution errored', properties); if (properties.is_manual) { - this.executionCountsBuffer[workflowId].manual_error_count++; + firstExecKey = 'first_manual_error'; + countKey = 'manual_error_count'; } else { - this.executionCountsBuffer[workflowId].prod_error_count++; + firstExecKey = 'first_prod_error'; + countKey = 'prod_error_count'; } } else if (properties.is_manual) { - this.executionCountsBuffer[workflowId].manual_success_count++; + countKey = 'manual_success_count'; + firstExecKey = 'first_manual_success'; } else { - this.executionCountsBuffer[workflowId].prod_success_count++; + countKey = 'prod_success_count'; + firstExecKey = 'first_prod_success'; } + + if ( + !this.executionCountsBuffer.firstExecutions[firstExecKey] && + this.executionCountsBuffer.counts[workflowId][countKey] === 0 + ) { + this.executionCountsBuffer.firstExecutions[firstExecKey] = new Date(); + } + + this.executionCountsBuffer.counts[workflowId][countKey]++; } } diff --git a/packages/editor-ui/src/App.vue b/packages/editor-ui/src/App.vue index 48d48b0958..1fba7209f0 100644 --- a/packages/editor-ui/src/App.vue +++ b/packages/editor-ui/src/App.vue @@ -14,14 +14,20 @@