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:
@@ -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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user