Improve telemetry (#2604)

* add node positions in node graph

* add hover events

* add tag count in save event

* populate properties when default

* fix delete and enable node events

* add node and workflow exec events

* lint

* add node graph

* add node id
This commit is contained in:
Ahsan Virani 2022-01-07 17:14:59 +01:00 committed by GitHub
parent df412e9523
commit 57016624b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 104 additions and 32 deletions

View file

@ -128,16 +128,16 @@ export class Execute extends Command {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
await externalHooks.init(); await externalHooks.init();
const instanceId = await UserSettings.getInstanceId();
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();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes); await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
const instanceId = await UserSettings.getInstanceId();
const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli, nodeTypes);
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) { if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
workflowId = undefined; workflowId = undefined;
} }

View file

@ -308,16 +308,16 @@ export class ExecuteBatch extends Command {
const externalHooks = ExternalHooks(); const externalHooks = ExternalHooks();
await externalHooks.init(); await externalHooks.init();
const instanceId = await UserSettings.getInstanceId();
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();
await nodeTypes.init(loadNodesAndCredentials.nodeTypes); await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
const credentialTypes = CredentialTypes(); const credentialTypes = CredentialTypes();
await credentialTypes.init(loadNodesAndCredentials.credentialTypes); await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
const instanceId = await UserSettings.getInstanceId();
const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli, nodeTypes);
// Send a shallow copy of allWorkflows so we still have all workflow data. // Send a shallow copy of allWorkflows so we still have all workflow data.
const results = await this.runTests([...allWorkflows]); const results = await this.runTests([...allWorkflows]);

View file

@ -303,7 +303,7 @@ export class Start extends Command {
const instanceId = await UserSettings.getInstanceId(); const instanceId = await UserSettings.getInstanceId();
const { cli } = await GenericHelpers.getVersions(); const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli); InternalHooksManager.init(instanceId, cli, nodeTypes);
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig; const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
await BinaryDataManager.init(binaryDataConfig, true); await BinaryDataManager.init(binaryDataConfig, true);

View file

@ -150,7 +150,7 @@ export class Webhook extends Command {
const instanceId = await UserSettings.getInstanceId(); const instanceId = await UserSettings.getInstanceId();
const { cli } = await GenericHelpers.getVersions(); const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli); InternalHooksManager.init(instanceId, cli, nodeTypes);
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig; const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
await BinaryDataManager.init(binaryDataConfig); await BinaryDataManager.init(binaryDataConfig);

View file

@ -274,11 +274,11 @@ export class Worker extends Command {
const versions = await GenericHelpers.getVersions(); const versions = await GenericHelpers.getVersions();
const instanceId = await UserSettings.getInstanceId(); const instanceId = await UserSettings.getInstanceId();
InternalHooksManager.init(instanceId, versions.cli, nodeTypes);
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig; const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
await BinaryDataManager.init(binaryDataConfig); await BinaryDataManager.init(binaryDataConfig);
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}`);
console.info(` * Concurrency: ${flags.concurrency}`); console.info(` * Concurrency: ${flags.concurrency}`);

View file

@ -1,19 +1,23 @@
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import { BinaryDataManager } from 'n8n-core'; import { BinaryDataManager } from 'n8n-core';
import { IDataObject, IRun, TelemetryHelpers } from 'n8n-workflow'; import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
import { import {
IDiagnosticInfo, IDiagnosticInfo,
IInternalHooksClass, IInternalHooksClass,
IPersonalizationSurveyAnswers, IPersonalizationSurveyAnswers,
IWorkflowBase, IWorkflowBase,
IWorkflowDb,
} from '.'; } from '.';
import { Telemetry } from './telemetry'; import { Telemetry } from './telemetry';
export class InternalHooksClass implements IInternalHooksClass { export class InternalHooksClass implements IInternalHooksClass {
private versionCli: string; private versionCli: string;
constructor(private telemetry: Telemetry, versionCli: string) { private nodeTypes: INodeTypes;
constructor(private telemetry: Telemetry, versionCli: string, nodeTypes: INodeTypes) {
this.versionCli = versionCli; this.versionCli = versionCli;
this.nodeTypes = nodeTypes;
} }
async onServerStarted( async onServerStarted(
@ -53,7 +57,7 @@ export class InternalHooksClass implements IInternalHooksClass {
} }
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> { async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow); const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
return this.telemetry.track('User created workflow', { return this.telemetry.track('User created workflow', {
workflow_id: workflow.id, workflow_id: workflow.id,
node_graph: nodeGraph, node_graph: nodeGraph,
@ -67,14 +71,15 @@ export class InternalHooksClass implements IInternalHooksClass {
}); });
} }
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> { async onWorkflowSaved(workflow: IWorkflowDb): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow); const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
return this.telemetry.track('User saved workflow', { return this.telemetry.track('User saved workflow', {
workflow_id: workflow.id, workflow_id: workflow.id,
node_graph: nodeGraph, node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph), node_graph_string: JSON.stringify(nodeGraph),
version_cli: this.versionCli, version_cli: this.versionCli,
num_tags: workflow.tags.length,
}); });
} }
@ -83,6 +88,7 @@ export class InternalHooksClass implements IInternalHooksClass {
workflow: IWorkflowBase, workflow: IWorkflowBase,
runData?: IRun, runData?: IRun,
): Promise<void> { ): Promise<void> {
const promises = [Promise.resolve()];
const properties: IDataObject = { const properties: IDataObject = {
workflow_id: workflow.id, workflow_id: workflow.id,
is_manual: false, is_manual: false,
@ -91,11 +97,10 @@ export class InternalHooksClass implements IInternalHooksClass {
if (runData !== undefined) { if (runData !== undefined) {
properties.execution_mode = runData.mode; properties.execution_mode = runData.mode;
if (runData.mode === 'manual') {
properties.is_manual = true;
}
properties.success = !!runData.finished; properties.success = !!runData.finished;
properties.is_manual = runData.mode === 'manual';
let nodeGraphResult;
if (!properties.success && runData?.data.resultData.error) { if (!properties.success && runData?.data.resultData.error) {
properties.error_message = runData?.data.resultData.error.message; properties.error_message = runData?.data.resultData.error.message;
@ -115,7 +120,7 @@ export class InternalHooksClass implements IInternalHooksClass {
} }
if (properties.is_manual) { if (properties.is_manual) {
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow); nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
properties.node_graph = nodeGraphResult.nodeGraph; properties.node_graph = nodeGraphResult.nodeGraph;
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph); properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
@ -124,9 +129,51 @@ export class InternalHooksClass implements IInternalHooksClass {
} }
} }
} }
if (properties.is_manual) {
if (!nodeGraphResult) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
}
const manualExecEventProperties = {
workflow_id: workflow.id,
status: properties.success ? 'success' : 'failed',
error_message: properties.error_message,
error_node_type: properties.error_node_type,
node_graph: properties.node_graph,
node_graph_string: properties.node_graph_string,
error_node_id: properties.error_node_id,
};
if (!manualExecEventProperties.node_graph) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
manualExecEventProperties.node_graph_string = JSON.stringify(
manualExecEventProperties.node_graph,
);
}
if (runData.data.startData?.destinationNode) {
promises.push(
this.telemetry.track('Manual node exec finished', {
...manualExecEventProperties,
node_type: TelemetryHelpers.getNodeTypeForName(
workflow,
runData.data.startData?.destinationNode,
)?.type,
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
}),
);
} else {
promises.push(
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
);
}
}
} }
return Promise.all([ return Promise.all([
...promises,
BinaryDataManager.getInstance().persistBinaryDataForExecutionId(executionId), BinaryDataManager.getInstance().persistBinaryDataForExecutionId(executionId),
this.telemetry.trackWorkflowExecution(properties), this.telemetry.trackWorkflowExecution(properties),
]).then(() => {}); ]).then(() => {});

View file

@ -1,4 +1,5 @@
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import { INodeTypes } from 'n8n-workflow';
import { InternalHooksClass } from './InternalHooks'; import { InternalHooksClass } from './InternalHooks';
import { Telemetry } from './telemetry'; import { Telemetry } from './telemetry';
@ -13,11 +14,12 @@ export class InternalHooksManager {
throw new Error('InternalHooks not initialized'); throw new Error('InternalHooks not initialized');
} }
static init(instanceId: string, versionCli: string): InternalHooksClass { static init(instanceId: string, versionCli: string, nodeTypes: INodeTypes): InternalHooksClass {
if (!this.internalHooksInstance) { if (!this.internalHooksInstance) {
this.internalHooksInstance = new InternalHooksClass( this.internalHooksInstance = new InternalHooksClass(
new Telemetry(instanceId, versionCli), new Telemetry(instanceId, versionCli),
versionCli, versionCli,
nodeTypes,
); );
} }

View file

@ -891,7 +891,7 @@ class App {
} }
await this.externalHooks.run('workflow.afterUpdate', [workflow]); await this.externalHooks.run('workflow.afterUpdate', [workflow]);
void InternalHooksManager.getInstance().onWorkflowSaved(workflow as IWorkflowBase); void InternalHooksManager.getInstance().onWorkflowSaved(workflow);
if (workflow.active) { if (workflow.active) {
// When the workflow is supposed to be active add it again // When the workflow is supposed to be active add it again

View file

@ -145,7 +145,7 @@ export class WorkflowRunnerProcess {
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? ''; const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
const { cli } = await GenericHelpers.getVersions(); const { cli } = await GenericHelpers.getVersions();
InternalHooksManager.init(instanceId, cli); InternalHooksManager.init(instanceId, cli, nodeTypes);
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig; const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
await BinaryDataManager.init(binaryDataConfig); await BinaryDataManager.init(binaryDataConfig);

View file

@ -328,14 +328,14 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
}, },
disableNode () { disableNode () {
this.disableNodes([this.data]); this.disableNodes([this.data]);
this.$telemetry.track('User set node enabled status', { node_type: this.data.type, is_enabled: !this.data.disabled, workflow_id: this.$store.getters.workflowId }); this.$telemetry.track('User clicked node hover button', { node_type: this.data.type, button_name: 'disable', workflow_id: this.$store.getters.workflowId });
}, },
executeNode () { executeNode () {
this.$emit('runWorkflow', this.data.name, 'Node.executeNode'); this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
this.$telemetry.track('User clicked node hover button', { node_type: this.data.type, button_name: 'execute', workflow_id: this.$store.getters.workflowId });
}, },
deleteNode () { deleteNode () {
this.$externalHooks().run('node.deleteNode', { node: this.data}); this.$telemetry.track('User clicked node hover button', { node_type: this.data.type, button_name: 'delete', workflow_id: this.$store.getters.workflowId });
this.$telemetry.track('User deleted node', { node_type: this.data.type, workflow_id: this.$store.getters.workflowId });
Vue.nextTick(() => { Vue.nextTick(() => {
// Wait a tick else vue causes problems because the data is gone // Wait a tick else vue causes problems because the data is gone
@ -343,6 +343,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
}); });
}, },
duplicateNode () { duplicateNode () {
this.$telemetry.track('User clicked node hover button', { node_type: this.data.type, button_name: 'duplicate', workflow_id: this.$store.getters.workflowId });
Vue.nextTick(() => { Vue.nextTick(() => {
// Wait a tick else vue causes problems because the data is gone // Wait a tick else vue causes problems because the data is gone
this.$emit('duplicateNode', this.data.name); this.$emit('duplicateNode', this.data.name);

View file

@ -343,6 +343,8 @@ export const nodeHelpers = mixins(
}, },
}; };
this.$telemetry.track('User set node enabled status', { node_type: node.type, is_enabled: node.disabled, workflow_id: this.$store.getters.workflowId });
this.$store.commit('updateNodeProperties', updateInformation); this.$store.commit('updateNodeProperties', updateInformation);
this.$store.commit('clearNodeExecutionData', node.name); this.$store.commit('clearNodeExecutionData', node.name);
this.updateNodeParameterIssues(node); this.updateNodeParameterIssues(node);

View file

@ -2133,6 +2133,9 @@ export default mixins(
} }
} }
this.$externalHooks().run('node.deleteNode', { node });
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
let waitForNewConnection = false; let waitForNewConnection = false;
// connect nodes before/after deleted node // connect nodes before/after deleted node
const nodeType: INodeTypeDescription | null = this.$store.getters.nodeType(node.type, node.typeVersion); const nodeType: INodeTypeDescription | null = this.$store.getters.nodeType(node.type, node.typeVersion);

View file

@ -1097,6 +1097,8 @@ export interface INodeGraphItem {
resource?: string; resource?: string;
operation?: string; operation?: string;
domain?: string; domain?: string;
position: [number, number];
mode?: string;
} }
export interface INodeNameIndex { export interface INodeNameIndex {

View file

@ -7,13 +7,18 @@ import {
INodeGraphItem, INodeGraphItem,
INodesGraphResult, INodesGraphResult,
IWorkflowBase, IWorkflowBase,
INodeTypes,
INodeType,
} from '.'; } from '.';
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined { export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
return workflow.nodes.find((node) => node.name === nodeName); return workflow.nodes.find((node) => node.name === nodeName);
} }
export function generateNodesGraph(workflow: IWorkflowBase): INodesGraphResult { export function generateNodesGraph(
workflow: IWorkflowBase,
nodeTypes: INodeTypes,
): INodesGraphResult {
const nodesGraph: INodesGraph = { const nodesGraph: INodesGraph = {
node_types: [], node_types: [],
node_connections: [], node_connections: [],
@ -25,6 +30,7 @@ export function generateNodesGraph(workflow: IWorkflowBase): INodesGraphResult {
nodesGraph.node_types.push(node.type); nodesGraph.node_types.push(node.type);
const nodeItem: INodeGraphItem = { const nodeItem: INodeGraphItem = {
type: node.type, type: node.type,
position: node.position,
}; };
if (node.type === 'n8n-nodes-base.httpRequest') { if (node.type === 'n8n-nodes-base.httpRequest') {
@ -34,11 +40,20 @@ export function generateNodesGraph(workflow: IWorkflowBase): INodesGraphResult {
nodeItem.domain = node.parameters.url as string; nodeItem.domain = node.parameters.url as string;
} }
} else { } else {
Object.keys(node.parameters).forEach((parameterName) => { const nodeType = nodeTypes.getByName(node.type) as INodeType;
if (parameterName === 'operation' || parameterName === 'resource') { nodeType.description.properties.forEach((property) => {
nodeItem[parameterName] = node.parameters[parameterName] as string; if (
property.name === 'operation' ||
property.name === 'resource' ||
property.name === 'mode'
) {
nodeItem[property.name] = property.default ? property.default.toString() : undefined;
} }
}); });
nodeItem.operation = node.parameters.operation?.toString() ?? nodeItem.operation;
nodeItem.resource = node.parameters.resource?.toString() ?? nodeItem.resource;
nodeItem.mode = node.parameters.mode?.toString() ?? nodeItem.mode;
} }
nodesGraph.nodes[`${index}`] = nodeItem; nodesGraph.nodes[`${index}`] = nodeItem;
nodeNameAndIndex[node.name] = index.toString(); nodeNameAndIndex[node.name] = index.toString();