Tweaks to diagnostic events (#2544)

* Tweaks to events

* more tweaks and fixes
This commit is contained in:
Ahsan Virani 2021-12-10 15:29:05 +01:00 committed by GitHub
parent 75c7b5ed97
commit 2125f25791
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 142 additions and 50 deletions

View file

@ -11,6 +11,7 @@ import {
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers,
InternalHooksManager, InternalHooksManager,
IWorkflowBase, IWorkflowBase,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
@ -125,7 +126,8 @@ export class Execute extends Command {
await externalHooks.init(); await externalHooks.init();
const instanceId = await UserSettings.getInstanceId(); 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 // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();

View file

@ -28,6 +28,7 @@ import {
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers,
InternalHooksManager, InternalHooksManager,
IWorkflowDb, IWorkflowDb,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
@ -305,7 +306,8 @@ export class ExecuteBatch extends Command {
await externalHooks.init(); await externalHooks.init();
const instanceId = await UserSettings.getInstanceId(); 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 // Add the found types to an instance other parts of the application can use
const nodeTypes = NodeTypes(); const nodeTypes = NodeTypes();

View file

@ -153,17 +153,6 @@ export class Start extends Command {
LoggerProxy.init(logger); LoggerProxy.init(logger);
logger.info('Initializing n8n process'); 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 // Start directly with the init of the database to improve startup time
const startDbInitPromise = Db.init().catch((error: Error) => { const startDbInitPromise = Db.init().catch((error: Error) => {
logger.error(`There was an error initializing DB: "${error.message}"`); logger.error(`There was an error initializing DB: "${error.message}"`);
@ -313,7 +302,8 @@ export class Start extends Command {
} }
const instanceId = await UserSettings.getInstanceId(); const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId); const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli);
await Server.start(); await Server.start();

View file

@ -149,7 +149,8 @@ export class Webhook extends Command {
await startDbInitPromise; await startDbInitPromise;
const instanceId = await UserSettings.getInstanceId(); const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId); const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli);
if (config.get('executions.mode') === 'queue') { if (config.get('executions.mode') === 'queue') {
const redisHost = config.get('queue.bull.redis.host'); const redisHost = config.get('queue.bull.redis.host');

View file

@ -271,10 +271,10 @@ export class Worker extends Command {
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes)); 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 versions = await GenericHelpers.getVersions();
const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId, versions.cli);
console.info('\nn8n worker is now ready'); console.info('\nn8n worker is now ready');
console.info(` * Version: ${versions.cli}`); console.info(` * Version: ${versions.cli}`);

View file

@ -314,7 +314,10 @@ export interface IDiagnosticInfo {
export interface IInternalHooksClass { export interface IInternalHooksClass {
onN8nStop(): Promise<void>; onN8nStop(): Promise<void>;
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]>; onServerStarted(
diagnosticInfo: IDiagnosticInfo,
firstWorkflowCreatedAt?: Date,
): Promise<unknown[]>;
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>; onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>; onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
onWorkflowDeleted(workflowId: string): Promise<void>; onWorkflowDeleted(workflowId: string): Promise<void>;

View file

@ -9,9 +9,16 @@ import {
import { Telemetry } from './telemetry'; import { Telemetry } from './telemetry';
export class InternalHooksClass implements IInternalHooksClass { export class InternalHooksClass implements IInternalHooksClass {
constructor(private telemetry: Telemetry) {} private versionCli: string;
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]> { constructor(private telemetry: Telemetry, versionCli: string) {
this.versionCli = versionCli;
}
async onServerStarted(
diagnosticInfo: IDiagnosticInfo,
earliestWorkflowCreatedAt?: Date,
): Promise<unknown[]> {
const info = { const info = {
version_cli: diagnosticInfo.versionCli, version_cli: diagnosticInfo.versionCli,
db_type: diagnosticInfo.databaseType, db_type: diagnosticInfo.databaseType,
@ -25,7 +32,10 @@ export class InternalHooksClass implements IInternalHooksClass {
return Promise.all([ return Promise.all([
this.telemetry.identify(info), 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<void> { async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
return this.telemetry.track('User created workflow', { return this.telemetry.track('User created workflow', {
workflow_id: workflow.id, 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<void> { async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
return this.telemetry.track('User saved workflow', { return this.telemetry.track('User saved workflow', {
workflow_id: workflow.id, 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 = { const properties: IDataObject = {
workflow_id: workflow.id, workflow_id: workflow.id,
is_manual: false, is_manual: false,
version_cli: this.versionCli,
}; };
if (runData !== undefined) { if (runData !== undefined) {
@ -92,6 +109,8 @@ export class InternalHooksClass implements IInternalHooksClass {
if (properties.is_manual) { if (properties.is_manual) {
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow); const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
properties.node_graph = nodeGraphResult.nodeGraph; properties.node_graph = nodeGraphResult.nodeGraph;
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
if (errorNodeName) { if (errorNodeName) {
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName]; properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
} }

View file

@ -13,9 +13,12 @@ export class InternalHooksManager {
throw new Error('InternalHooks not initialized'); throw new Error('InternalHooks not initialized');
} }
static init(instanceId: string): InternalHooksClass { static init(instanceId: string, versionCli: string): InternalHooksClass {
if (!this.internalHooksInstance) { if (!this.internalHooksInstance) {
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId)); this.internalHooksInstance = new InternalHooksClass(
new Telemetry(instanceId, versionCli),
versionCli,
);
} }
return this.internalHooksInstance; return this.internalHooksInstance;

View file

@ -2896,7 +2896,14 @@ export async function start(): Promise<void> {
deploymentType: config.get('deployment.type'), 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),
);
}); });
} }

View file

@ -31,6 +31,7 @@ import {
CredentialTypes, CredentialTypes,
Db, Db,
ExternalHooks, ExternalHooks,
GenericHelpers,
IWorkflowExecuteProcess, IWorkflowExecuteProcess,
IWorkflowExecutionDataProcessWithExecution, IWorkflowExecutionDataProcessWithExecution,
NodeTypes, NodeTypes,
@ -137,7 +138,8 @@ export class WorkflowRunnerProcess {
await externalHooks.init(); await externalHooks.init();
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? ''; 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. // Credentials should now be loaded from database.
// We check if any node uses credentials. If it does, then // We check if any node uses credentials. If it does, then

View file

@ -5,28 +5,57 @@ import { IDataObject, LoggerProxy } from 'n8n-workflow';
import config = require('../../config'); import config = require('../../config');
import { getLogger } from '../Logger'; import { getLogger } from '../Logger';
interface IExecutionCountsBufferItem { type CountBufferItemKey =
manual_success_count: number; | 'manual_success_count'
manual_error_count: number; | 'manual_error_count'
prod_success_count: number; | 'prod_success_count'
prod_error_count: number; | '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 { interface IExecutionCountsBuffer {
[workflowId: string]: IExecutionCountsBufferItem; [workflowId: string]: IExecutionCountsBufferItem;
} }
type IFirstExecutions = {
[key in FirstExecutionItemKey]: Date | undefined;
};
interface IExecutionsBuffer {
counts: IExecutionCountsBuffer;
firstExecutions: IFirstExecutions;
}
export class Telemetry { export class Telemetry {
private client?: TelemetryClient; private client?: TelemetryClient;
private instanceId: string; private instanceId: string;
private versionCli: string;
private pulseIntervalReference: NodeJS.Timeout; 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.instanceId = instanceId;
this.versionCli = versionCli;
const enabled = config.get('diagnostics.enabled') as boolean; const enabled = config.get('diagnostics.enabled') as boolean;
if (enabled) { if (enabled) {
@ -53,33 +82,41 @@ export class Telemetry {
return Promise.resolve(); 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', { const promise = this.track('Workflow execution count', {
version_cli: this.versionCli,
workflow_id: workflowId, 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.counts[workflowId].manual_error_count = 0;
this.executionCountsBuffer[workflowId].prod_error_count = 0; this.executionCountsBuffer.counts[workflowId].manual_success_count = 0;
this.executionCountsBuffer[workflowId].prod_success_count = 0; this.executionCountsBuffer.counts[workflowId].prod_error_count = 0;
this.executionCountsBuffer.counts[workflowId].prod_success_count = 0;
return promise; return promise;
}); });
allPromises.push(this.track('pulse')); allPromises.push(this.track('pulse', { version_cli: this.versionCli }));
return Promise.all(allPromises); return Promise.all(allPromises);
} }
async trackWorkflowExecution(properties: IDataObject): Promise<void> { async trackWorkflowExecution(properties: IDataObject): Promise<void> {
if (this.client) { if (this.client) {
const workflowId = properties.workflow_id as string; 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_error_count: 0,
manual_success_count: 0, manual_success_count: 0,
prod_error_count: 0, prod_error_count: 0,
prod_success_count: 0, prod_success_count: 0,
}; };
let countKey: CountBufferItemKey;
let firstExecKey: FirstExecutionItemKey;
if ( if (
properties.success === false && properties.success === false &&
properties.error_node_type && properties.error_node_type &&
@ -89,15 +126,28 @@ export class Telemetry {
void this.track('Workflow execution errored', properties); void this.track('Workflow execution errored', properties);
if (properties.is_manual) { if (properties.is_manual) {
this.executionCountsBuffer[workflowId].manual_error_count++; firstExecKey = 'first_manual_error';
countKey = 'manual_error_count';
} else { } else {
this.executionCountsBuffer[workflowId].prod_error_count++; firstExecKey = 'first_prod_error';
countKey = 'prod_error_count';
} }
} else if (properties.is_manual) { } else if (properties.is_manual) {
this.executionCountsBuffer[workflowId].manual_success_count++; countKey = 'manual_success_count';
firstExecKey = 'first_manual_success';
} else { } 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]++;
} }
} }

View file

@ -14,14 +14,20 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue from 'vue';
import Telemetry from './components/Telemetry.vue'; import Telemetry from './components/Telemetry.vue';
export default { export default Vue.extend({
name: 'App', name: 'App',
components: { components: {
Telemetry, Telemetry,
}, },
}; watch: {
'$route'(route) {
this.$telemetry.page('Editor', route.name);
},
},
});
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -66,6 +66,12 @@ class Telemetry {
} }
} }
page(category?: string, name?: string | undefined | null) {
if (this.telemetry) {
this.telemetry.page(category, name);
}
}
trackNodesPanel(event: string, properties: IDataObject = {}) { trackNodesPanel(event: string, properties: IDataObject = {}) {
if (this.telemetry) { if (this.telemetry) {
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId; properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;

View file

@ -2526,6 +2526,7 @@ export default mixins(
}); });
this.$externalHooks().run('nodeView.mount'); this.$externalHooks().run('nodeView.mount');
this.$telemetry.page('Editor', this.$route.name);
}, },
destroyed () { destroyed () {