refactor: Telemetry updates (#3529)

* Init unit tests for telemetry

* Update telemetry tests

* Test Workflow execution errored event

* Add new tracking logic in pulse

* cleanup

* interfaces

* Add event_version for Workflow execution count event

* add version_cli in all events

* add user saved credentials event

* update manual wf exec finished, fixes

* improve typings, lint

* add node_graph_string in User clicked execute workflow button event

* add User set node operation or mode event

* Add instance started event in FE

* Add User clicked retry execution button event

* add expression editor event

* add input node type to add node event

* add User stopped workflow execution wvent

* add error message in saved credential event

* update stop execution event

* add execution preflight event

* Remove instance started even tfrom FE, add session started to FE,BE

* improve typing

* remove node_graph as property from all events

* move back from default export

* move psl npm package to cli package

* cr

* update webhook node domain logic

* fix is_valid for User saved credentials event

* fix Expression Editor variable selector event

* add caused_by_credential in preflight event

* undo webhook_domain

* change node_type to full type

* add webhook_domain property in manual execution event (#3680)

* add webhook_domain property in manual execution event

* lint fix
This commit is contained in:
Ahsan Virani
2022-07-10 08:53:04 +02:00
committed by GitHub
parent 32c68eb126
commit 6b2db8e4f4
18 changed files with 719 additions and 139 deletions

View File

@@ -112,6 +112,7 @@ import {
INodeParameters,
INodeProperties,
INodeTypeDescription,
ITelemetryTrackProperties,
NodeHelpers,
} from 'n8n-workflow';
import CredentialIcon from '../CredentialIcon.vue';
@@ -620,7 +621,9 @@ export default mixins(showMessage, nodeHelpers).extend({
let credential;
if (this.mode === 'new' && !this.credentialId) {
const isNewCredential = this.mode === 'new' && !this.credentialId;
if (isNewCredential) {
credential = await this.createCredential(
credentialDetails,
);
@@ -647,6 +650,30 @@ export default mixins(showMessage, nodeHelpers).extend({
this.authError = '';
this.testedSuccessfully = false;
}
const trackProperties: ITelemetryTrackProperties = {
credential_type: credentialDetails.type,
workflow_id: this.$store.getters.workflowId,
credential_id: credential.id,
is_complete: !!this.requiredPropertiesFilled,
is_new: isNewCredential,
};
if (this.isOAuthType) {
trackProperties.is_valid = !!this.isOAuthConnected;
} else if (this.isCredentialTestable) {
trackProperties.is_valid = !!this.testedSuccessfully;
}
if (this.$store.getters.activeNode) {
trackProperties.node_type = this.$store.getters.activeNode.type;
}
if (this.authError && this.authError !== '') {
trackProperties.authError = this.authError;
}
this.$telemetry.track('User saved credentials', trackProperties);
}
return credential;

View File

@@ -435,6 +435,12 @@ export default mixins(
}
this.retryExecution(commandData.row, loadWorkflow);
this.$telemetry.track('User clicked retry execution button', {
workflow_id: this.$store.getters.workflowId,
execution_id: commandData.row.id,
retry_type: loadWorkflow ? 'current' : 'original',
});
},
getRowClass (data: IDataObject): string {
const classes: string[] = [];

View File

@@ -102,6 +102,59 @@ export default mixins(
itemSelected (eventData: IVariableItemSelected) {
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
const trackProperties: {
event_version: string;
node_type_dest: string;
node_type_source?: string;
parameter_name_dest: string;
parameter_name_source?: string;
variable_type?: string;
is_immediate_input: boolean;
variable_expression: string;
node_name: string;
} = {
event_version: '2',
node_type_dest: this.$store.getters.activeNode.type,
parameter_name_dest: this.parameter.displayName,
is_immediate_input: false,
variable_expression: eventData.variable,
node_name: this.$store.getters.activeNode.name,
};
if (eventData.variable) {
let splitVar = eventData.variable.split('.');
if (eventData.variable.startsWith('Object.keys')) {
splitVar = eventData.variable.split('(')[1].split(')')[0].split('.');
trackProperties.variable_type = 'Keys';
} else if (eventData.variable.startsWith('Object.values')) {
splitVar = eventData.variable.split('(')[1].split(')')[0].split('.');
trackProperties.variable_type = 'Values';
} else {
trackProperties.variable_type = 'Raw value';
}
if (splitVar[0].startsWith('$node')) {
const sourceNodeName = splitVar[0].split('"')[1];
trackProperties.node_type_source = this.$store.getters.getNodeByName(sourceNodeName).type;
const nodeConnections: Array<Array<{ node: string }>> = this.$store.getters.outgoingConnectionsByNodeName(sourceNodeName).main;
trackProperties.is_immediate_input = (nodeConnections && nodeConnections[0] && !!nodeConnections[0].find(({ node }) => node === this.$store.getters.activeNode.name)) ? true : false;
if (splitVar[1].startsWith('parameter')) {
trackProperties.parameter_name_source = splitVar[1].split('"')[1];
}
} else {
trackProperties.is_immediate_input = true;
if(splitVar[0].startsWith('$parameter')) {
trackProperties.parameter_name_source = splitVar[0].split('"')[1];
}
}
}
this.$telemetry.track('User inserted item from Expression Editor variable selector', trackProperties);
},
},
watch: {

View File

@@ -853,6 +853,17 @@ export default mixins(
};
this.$emit('valueChanged', parameterData);
if (this.parameter.name === 'operation' || this.parameter.name === 'mode') {
this.$telemetry.track('User set node operation or mode', {
workflow_id: this.$store.getters.workflowId,
node_type: this.node && this.node.type,
resource: this.node && this.node.parameters.resource,
is_custom: value === CUSTOM_API_CALL_KEY,
session_id: this.$store.getters['ui/ndvSessionId'],
parameter: this.parameter.name,
});
}
},
optionSelected (command: string) {
if (command === 'resetValue') {

View File

@@ -258,11 +258,7 @@ export const workflowHelpers = mixins(
return workflowIssues;
},
// Returns a workflow instance.
getWorkflow (nodes?: INodeUi[], connections?: IConnections, copyData?: boolean): Workflow {
nodes = nodes || this.getNodes();
connections = connections || (this.$store.getters.allConnections as IConnections);
getNodeTypes (): INodeTypes {
const nodeTypes: INodeTypes = {
nodeTypes: {},
init: async (nodeTypes?: INodeTypeData): Promise<void> => { },
@@ -287,6 +283,15 @@ export const workflowHelpers = mixins(
},
};
return nodeTypes;
},
// Returns a workflow instance.
getWorkflow (nodes?: INodeUi[], connections?: IConnections, copyData?: boolean): Workflow {
nodes = nodes || this.getNodes();
connections = connections || (this.$store.getters.allConnections as IConnections);
const nodeTypes = this.getNodeTypes();
let workflowId = this.$store.getters.workflowId;
if (workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
workflowId = undefined;

View File

@@ -7,7 +7,9 @@ import {
import {
IRunData,
IRunExecutionData,
IWorkflowBase,
NodeHelpers,
TelemetryHelpers,
} from 'n8n-workflow';
import { externalHooks } from '@/components/mixins/externalHooks';
@@ -77,11 +79,32 @@ export const workflowRun = mixins(
if (workflowIssues !== null) {
const errorMessages = [];
let nodeIssues: string[];
const trackNodeIssues: Array<{
node_type: string;
error: string;
}> = [];
const trackErrorNodeTypes: string[] = [];
for (const nodeName of Object.keys(workflowIssues)) {
nodeIssues = NodeHelpers.nodeIssuesToString(workflowIssues[nodeName]);
let issueNodeType = 'UNKNOWN';
const issueNode = this.$store.getters.getNodeByName(nodeName);
if (issueNode) {
issueNodeType = issueNode.type;
}
trackErrorNodeTypes.push(issueNodeType);
const trackNodeIssue = {
node_type: issueNodeType,
error: '',
caused_by_credential: !!workflowIssues[nodeName].credentials,
};
for (const nodeIssue of nodeIssues) {
errorMessages.push(`${nodeName}: ${nodeIssue}`);
trackNodeIssue.error = trackNodeIssue.error.concat(', ', nodeIssue);
}
trackNodeIssues.push(trackNodeIssue);
}
this.$showMessage({
@@ -92,6 +115,17 @@ export const workflowRun = mixins(
});
this.$titleSet(workflow.name as string, 'ERROR');
this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName });
this.getWorkflowDataToSave().then((workflowData) => {
this.$telemetry.track('Workflow execution preflight failed', {
workflow_id: workflow.id,
workflow_name: workflow.name,
execution_type: nodeName ? 'node' : 'workflow',
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
error_node_types: JSON.stringify(trackErrorNodeTypes),
errors: JSON.stringify(trackNodeIssues),
});
});
return;
}
}

View File

@@ -1,6 +1,7 @@
import _Vue from "vue";
import {
ITelemetrySettings,
ITelemetryTrackProperties,
IDataObject,
} from 'n8n-workflow';
import { ILogLevel, INodeCreateElement, IRootState } from "@/Interface";
@@ -72,6 +73,7 @@ class Telemetry {
this.loadTelemetryLibrary(options.config.key, options.config.url, { integrations: { All: false }, loadIntegration: false, ...logging});
this.identify(instanceId, userId);
this.flushPageEvents();
this.track('Session started', { session_id: store.getters.sessionId });
}
}
@@ -86,9 +88,14 @@ class Telemetry {
}
}
track(event: string, properties?: IDataObject) {
track(event: string, properties?: ITelemetryTrackProperties) {
if (this.telemetry) {
this.telemetry.track(event, properties);
const updatedProperties = {
...properties,
version_cli: this.store && this.store.getters.versionCli,
};
this.telemetry.track(event, updatedProperties);
}
}
@@ -131,21 +138,21 @@ class Telemetry {
if (properties.createNodeActive !== false) {
this.resetNodesPanelSession();
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
this.telemetry.track('User opened nodes panel', properties);
this.track('User opened nodes panel', properties);
}
break;
case 'nodeCreateList.selectedTypeChanged':
this.userNodesPanelSession.data.filterMode = properties.new_filter as string;
this.telemetry.track('User changed nodes panel filter', properties);
this.track('User changed nodes panel filter', properties);
break;
case 'nodeCreateList.destroyed':
if(this.userNodesPanelSession.data.nodeFilter.length > 0 && this.userNodesPanelSession.data.nodeFilter !== '') {
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
}
break;
case 'nodeCreateList.nodeFilterChanged':
if((properties.newValue as string).length === 0 && this.userNodesPanelSession.data.nodeFilter.length > 0) {
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
this.track('User entered nodes panel search term', this.generateNodesPanelEvent());
}
if((properties.newValue as string).length > (properties.oldValue as string || '').length) {
@@ -155,7 +162,7 @@ class Telemetry {
break;
case 'nodeCreateList.onCategoryExpanded':
properties.is_subcategory = false;
this.telemetry.track('User viewed node category', properties);
this.track('User viewed node category', properties);
break;
case 'nodeCreateList.onSubcategorySelected':
const selectedProperties = (properties.selected as IDataObject).properties as IDataObject;
@@ -164,13 +171,13 @@ class Telemetry {
}
properties.is_subcategory = true;
delete properties.selected;
this.telemetry.track('User viewed node category', properties);
this.track('User viewed node category', properties);
break;
case 'nodeView.addNodeButton':
this.telemetry.track('User added node to workflow canvas', properties);
this.track('User added node to workflow canvas', properties);
break;
case 'nodeView.addSticky':
this.telemetry.track('User inserted workflow note', properties);
this.track('User inserted workflow note', properties);
break;
default:
break;

View File

@@ -193,6 +193,9 @@ import {
IRun,
ITaskData,
INodeCredentialsDetails,
TelemetryHelpers,
ITelemetryTrackProperties,
IWorkflowBase,
} from 'n8n-workflow';
import {
ICredentialsResponse,
@@ -409,7 +412,13 @@ export default mixins(
this.runWorkflow(nodeName, source);
},
onRunWorkflow() {
this.$telemetry.track('User clicked execute workflow button', { workflow_id: this.$store.getters.workflowId });
this.getWorkflowDataToSave().then((workflowData) => {
this.$telemetry.track('User clicked execute workflow button', {
workflow_id: this.$store.getters.workflowId,
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
});
});
this.runWorkflow();
},
onCreateMenuHoverIn(mouseinEvent: MouseEvent) {
@@ -1169,6 +1178,15 @@ export default mixins(
}
}
this.stopExecutionInProgress = false;
this.getWorkflowDataToSave().then((workflowData) => {
const trackProps = {
workflow_id: this.$store.getters.workflowId,
node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
};
this.$telemetry.track('User clicked stop workflow execution', trackProps);
});
},
async stopWaitingForWebhook () {
@@ -1501,11 +1519,17 @@ export default mixins(
this.$telemetry.trackNodesPanel('nodeView.addSticky', { workflow_id: this.$store.getters.workflowId });
} else {
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
const trackProperties: ITelemetryTrackProperties = {
node_type: nodeTypeName,
workflow_id: this.$store.getters.workflowId,
drag_and_drop: options.dragAndDrop,
} as IDataObject);
};
if (lastSelectedNode) {
trackProperties.input_node_type = lastSelectedNode.type;
}
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', trackProperties);
}
// Automatically deselect all nodes and select the current one and also active