✨ Introduce telemetry (#2099)
* introduce analytics * add user survey backend * add user survey backend * set answers on survey submit Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * change name to personalization * lint Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * N8n 2495 add personalization modal (#2280) * update modals * add onboarding modal * implement questions * introduce analytics * simplify impl * implement survey handling * add personalized cateogry * update modal behavior * add thank you view * handle empty cases * rename modal * standarize modal names * update image, add tags to headings * remove unused file * remove unused interfaces * clean up footer spacing * introduce analytics * refactor to fix bug * update endpoint * set min height * update stories * update naming from questions to survey * remove spacing after core categories * fix bug in logic * sort nodes * rename types * merge with be * rename userSurvey * clean up rest api * use constants for keys * use survey keys * clean up types * move personalization to its own file Co-authored-by: ahsan-virani <ahsan.virani@gmail.com> * Survey new options (#2300) * split up options * fix quotes * remove unused import * add user created workflow event (#2301) * simplify env vars * fix versionCli on FE * update personalization env * fix event User opened Credentials panel * fix select modal spacing * fix nodes panel event * fix workflow id in workflow execute event * improve telemetry error logging * fix config and stop process events * add flush call on n8n stop * ready for release * improve telemetry process exit * fix merge * improve n8n stop events Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
@@ -87,7 +87,6 @@ export default Vue.extend({
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.subcategory + .category,
|
||||
.node + .category {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ export default mixins(externalHooks).extend({
|
||||
const nodeTypes: INodeCreateElement[] = this.searchItems;
|
||||
const filter = this.searchFilter;
|
||||
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
|
||||
const nodeType = (el.properties as INodeItemProps).nodeType;
|
||||
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
||||
});
|
||||
|
||||
@@ -152,12 +151,24 @@ export default mixins(externalHooks).extend({
|
||||
selectedType: this.selectedType,
|
||||
filteredNodes: this.filteredNodeTypes,
|
||||
});
|
||||
this.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
||||
oldValue,
|
||||
newValue,
|
||||
selectedType: this.selectedType,
|
||||
filteredNodes: this.filteredNodeTypes,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
});
|
||||
},
|
||||
selectedType(newValue, oldValue) {
|
||||
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
||||
oldValue,
|
||||
newValue,
|
||||
});
|
||||
this.$telemetry.trackNodesPanel('nodeCreateList.selectedTypeChanged', {
|
||||
old_filter: oldValue,
|
||||
new_filter: newValue,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -243,6 +254,7 @@ export default mixins(externalHooks).extend({
|
||||
);
|
||||
} else {
|
||||
this.activeCategory = [...this.activeCategory, category];
|
||||
this.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { category_name: category, workflow_id: this.$store.getters.workflowId });
|
||||
}
|
||||
|
||||
this.activeIndex = this.categorized.findIndex(
|
||||
@@ -252,6 +264,7 @@ export default mixins(externalHooks).extend({
|
||||
onSubcategorySelected(selected: INodeCreateElement) {
|
||||
this.activeSubcategoryIndex = 0;
|
||||
this.activeSubcategory = selected;
|
||||
this.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { selected, workflow_id: this.$store.getters.workflowId });
|
||||
},
|
||||
|
||||
onSubcategoryClose() {
|
||||
@@ -273,6 +286,7 @@ export default mixins(externalHooks).extend({
|
||||
},
|
||||
async destroyed() {
|
||||
this.$externalHooks().run('nodeCreateList.destroyed');
|
||||
this.$telemetry.trackNodesPanel('nodeCreateList.destroyed', { workflow_id: this.$store.getters.workflowId });
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { HTTP_REQUEST_NODE_NAME, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_NAME } from '@/constants';
|
||||
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
import NoResultsIcon from './NoResultsIcon.vue';
|
||||
@@ -57,11 +57,11 @@ export default Vue.extend({
|
||||
},
|
||||
methods: {
|
||||
selectWebhook() {
|
||||
this.$emit('nodeTypeSelected', WEBHOOK_NODE_NAME);
|
||||
this.$emit('nodeTypeSelected', WEBHOOK_NODE_TYPE);
|
||||
},
|
||||
|
||||
selectHttpRequest() {
|
||||
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_NAME);
|
||||
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_TYPE);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import { HIDDEN_NODES } from '@/constants';
|
||||
|
||||
import MainPanel from './MainPanel.vue';
|
||||
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'NodeCreator',
|
||||
@@ -35,6 +36,7 @@ export default Vue.extend({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('settings', ['personalizedNodeTypes']),
|
||||
nodeTypes(): INodeTypeDescription[] {
|
||||
return this.$store.getters.allNodeTypes;
|
||||
},
|
||||
@@ -57,7 +59,7 @@ export default Vue.extend({
|
||||
}, []);
|
||||
},
|
||||
categoriesWithNodes(): ICategoriesWithNodes {
|
||||
return getCategoriesWithNodes(this.visibleNodeTypes);
|
||||
return getCategoriesWithNodes(this.visibleNodeTypes, this.personalizedNodeTypes as string[]);
|
||||
},
|
||||
categorizedItems(): INodeCreateElement[] {
|
||||
return getCategorizedList(this.categoriesWithNodes);
|
||||
|
||||
@@ -1,25 +1,51 @@
|
||||
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER } from '@/constants';
|
||||
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER, PERSONALIZED_CATEGORY } from '@/constants';
|
||||
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
|
||||
if (!accu[category]) {
|
||||
accu[category] = {};
|
||||
}
|
||||
if (!accu[category][subcategory]) {
|
||||
accu[category][subcategory] = {
|
||||
triggerCount: 0,
|
||||
regularCount: 0,
|
||||
nodes: [],
|
||||
};
|
||||
}
|
||||
const isTrigger = nodeType.group.includes('trigger');
|
||||
if (isTrigger) {
|
||||
accu[category][subcategory].triggerCount++;
|
||||
}
|
||||
if (!isTrigger) {
|
||||
accu[category][subcategory].regularCount++;
|
||||
}
|
||||
accu[category][subcategory].nodes.push({
|
||||
type: 'node',
|
||||
key: `${category}_${nodeType.name}`,
|
||||
category,
|
||||
properties: {
|
||||
nodeType,
|
||||
subcategory,
|
||||
},
|
||||
includedByTrigger: isTrigger,
|
||||
includedByRegular: !isTrigger,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICategoriesWithNodes => {
|
||||
return nodeTypes.reduce(
|
||||
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
|
||||
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
|
||||
return sorted.reduce(
|
||||
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
||||
if (personalizedNodeTypes.includes(nodeType.name)) {
|
||||
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||
}
|
||||
|
||||
if (!nodeType.codex || !nodeType.codex.categories) {
|
||||
accu[UNCATEGORIZED_CATEGORY][UNCATEGORIZED_SUBCATEGORY].nodes.push({
|
||||
type: 'node',
|
||||
category: UNCATEGORIZED_CATEGORY,
|
||||
key: `${UNCATEGORIZED_CATEGORY}_${nodeType.name}`,
|
||||
properties: {
|
||||
subcategory: UNCATEGORIZED_SUBCATEGORY,
|
||||
nodeType,
|
||||
},
|
||||
includedByTrigger: nodeType.group.includes('trigger'),
|
||||
includedByRegular: !nodeType.group.includes('trigger'),
|
||||
});
|
||||
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||
return accu;
|
||||
}
|
||||
|
||||
nodeType.codex.categories.forEach((_category: string) => {
|
||||
const category = _category.trim();
|
||||
const subcategory =
|
||||
@@ -28,58 +54,25 @@ export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICate
|
||||
nodeType.codex.subcategories[category]
|
||||
? nodeType.codex.subcategories[category][0]
|
||||
: UNCATEGORIZED_SUBCATEGORY;
|
||||
if (!accu[category]) {
|
||||
accu[category] = {};
|
||||
}
|
||||
if (!accu[category][subcategory]) {
|
||||
accu[category][subcategory] = {
|
||||
triggerCount: 0,
|
||||
regularCount: 0,
|
||||
nodes: [],
|
||||
};
|
||||
}
|
||||
const isTrigger = nodeType.group.includes('trigger');
|
||||
if (isTrigger) {
|
||||
accu[category][subcategory].triggerCount++;
|
||||
}
|
||||
if (!isTrigger) {
|
||||
accu[category][subcategory].regularCount++;
|
||||
}
|
||||
accu[category][subcategory].nodes.push({
|
||||
type: 'node',
|
||||
key: `${category}_${nodeType.name}`,
|
||||
category,
|
||||
properties: {
|
||||
nodeType,
|
||||
subcategory,
|
||||
},
|
||||
includedByTrigger: isTrigger,
|
||||
includedByRegular: !isTrigger,
|
||||
});
|
||||
|
||||
addNodeToCategory(accu, nodeType, category, subcategory);
|
||||
});
|
||||
return accu;
|
||||
},
|
||||
{
|
||||
[UNCATEGORIZED_CATEGORY]: {
|
||||
[UNCATEGORIZED_SUBCATEGORY]: {
|
||||
triggerCount: 0,
|
||||
regularCount: 0,
|
||||
nodes: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
||||
const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
|
||||
const categories = Object.keys(categoriesWithNodes);
|
||||
const sorted = categories.filter(
|
||||
(category: string) =>
|
||||
category !== CORE_NODES_CATEGORY && category !== CUSTOM_NODES_CATEGORY && category !== UNCATEGORIZED_CATEGORY,
|
||||
!excludeFromSort.includes(category),
|
||||
);
|
||||
sorted.sort();
|
||||
|
||||
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
||||
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
||||
};
|
||||
|
||||
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
||||
@@ -173,4 +166,4 @@ export const matchesNodeType = (el: INodeCreateElement, filter: string) => {
|
||||
const nodeType = (el.properties as INodeItemProps).nodeType;
|
||||
|
||||
return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter);
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user