diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue index 0c2816e2d8..728215c02c 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue @@ -29,6 +29,7 @@ import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue' import type { IDataObject } from 'n8n-workflow'; import { useTelemetry } from '@/composables/useTelemetry'; import { useI18n } from '@/composables/useI18n'; +import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; const emit = defineEmits<{ nodeTypeSelected: [value: [actionKey: string, nodeName: string] | [nodeName: string]]; @@ -47,6 +48,8 @@ const { actionsCategoryLocales, } = useActions(); +const nodeCreatorStore = useNodeCreatorStore(); + // We only inject labels if search is empty const parsedTriggerActions = computed(() => parseActions(actions.value, actionsCategoryLocales.value.triggers, false), @@ -182,7 +185,7 @@ function trackActionsView() { }; void useExternalHooks().run('nodeCreateList.onViewActions', trackingPayload); - telemetry?.trackNodesPanel('nodeCreateList.onViewActions', trackingPayload); + nodeCreatorStore.onViewActions(trackingPayload); } function resetSearch() { @@ -206,7 +209,7 @@ function addHttpNode() { void useExternalHooks().run('nodeCreateList.onActionsCustmAPIClicked', { app_identifier, }); - telemetry?.trackNodesPanel('nodeCreateList.onActionsCustmAPIClicked', { app_identifier }); + nodeCreatorStore.onActionsCustomAPIClicked({ app_identifier }); } // Anonymous component to handle triggers and actions rendering order diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue index 08f8d14cbb..ce833afa17 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue @@ -23,7 +23,6 @@ import ItemsRenderer from '../Renderers/ItemsRenderer.vue'; import CategorizedItemsRenderer from '../Renderers/CategorizedItemsRenderer.vue'; import NoResults from '../Panel/NoResults.vue'; import { useI18n } from '@/composables/useI18n'; -import { useTelemetry } from '@/composables/useTelemetry'; import { getNodeIcon, getNodeIconColor, getNodeIconUrl } from '@/utils/nodeTypesUtils'; import { useUIStore } from '@/stores/ui.store'; @@ -36,11 +35,10 @@ const emit = defineEmits<{ }>(); const i18n = useI18n(); -const telemetry = useTelemetry(); const uiStore = useUIStore(); const rootStore = useRootStore(); -const { mergedNodes, actions } = useNodeCreatorStore(); +const { mergedNodes, actions, onSubcategorySelected } = useNodeCreatorStore(); const { pushViewStack, popViewStack } = useViewStacks(); const { registerKeyHook } = useKeyboardNavigation(); @@ -83,7 +81,7 @@ function onSelected(item: INodeCreateElement) { sections: item.properties.sections, }); - telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { + onSubcategorySelected({ subcategory: item.key, }); } @@ -153,9 +151,6 @@ function onSelected(item: INodeCreateElement) { if (item.type === 'link') { window.open(item.properties.url, '_blank'); - telemetry.trackNodesPanel('nodeCreateList.onLinkSelected', { - link: item.properties.url, - }); } } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue index 72e31851f9..6cb414515d 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue @@ -17,12 +17,15 @@ import SearchBar from './SearchBar.vue'; import ActionsRenderer from '../Modes/ActionsMode.vue'; import NodesRenderer from '../Modes/NodesMode.vue'; import { useI18n } from '@/composables/useI18n'; +import { useDebounce } from '@/composables/useDebounce'; const i18n = useI18n(); +const { callDebounced } = useDebounce(); const { mergedNodes } = useNodeCreatorStore(); const { pushViewStack, popViewStack, updateCurrentViewStack } = useViewStacks(); const { setActiveItemIndex, attachKeydownEvent, detachKeydownEvent } = useKeyboardNavigation(); +const nodeCreatorStore = useNodeCreatorStore(); const activeViewStack = computed(() => useViewStacks().activeViewStack); @@ -55,6 +58,19 @@ function onSearch(value: string) { if (activeViewStack.value.uuid) { updateCurrentViewStack({ search: value }); void setActiveItemIndex(getDefaultActiveIndex(value)); + if (value.length) { + callDebounced( + nodeCreatorStore.onNodeFilterChanged, + { trailing: true, debounceTime: 2000 }, + { + newValue: value, + filteredNodes: activeViewStack.value.items ?? [], + filterMode: activeViewStack.value.rootView ?? 'Regular', + subcategory: activeViewStack.value.subcategory, + title: activeViewStack.value.title, + }, + ); + } } } @@ -299,6 +315,7 @@ function onBackButton() { margin-top: var(--spacing-4xs); font-size: var(--font-size-s); line-height: 19px; + color: var(--color-text-base); font-weight: var(--font-weight-regular); } diff --git a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue index 93b542c72a..b493d53680 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue +++ b/packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue @@ -8,7 +8,7 @@ import { useKeyboardNavigation } from '../composables/useKeyboardNavigation'; import { useViewStacks } from '../composables/useViewStacks'; import ItemsRenderer from './ItemsRenderer.vue'; import CategoryItem from '../ItemTypes/CategoryItem.vue'; -import { useTelemetry } from '@/composables/useTelemetry'; +import { useNodeCreatorStore } from '@/stores/nodeCreator.store'; export interface Props { elements: INodeCreateElement[]; @@ -24,10 +24,10 @@ const props = withDefaults(defineProps(), { elements: () => [], }); -const telemetry = useTelemetry(); const { popViewStack } = useViewStacks(); const { registerKeyHook } = useKeyboardNavigation(); const { workflowId } = useWorkflowsStore(); +const nodeCreatorStore = useNodeCreatorStore(); const activeItemId = computed(() => useKeyboardNavigation()?.activeItemId); const actionCount = computed(() => props.elements.filter(({ type }) => type === 'action').length); @@ -38,10 +38,11 @@ function toggleExpanded() { } function setExpanded(isExpanded: boolean) { + const prev = expanded.value; expanded.value = isExpanded; - if (expanded.value) { - telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { + if (expanded.value && !prev) { + nodeCreatorStore.onCategoryExpanded({ category_name: props.category, workflow_id: workflowId, }); diff --git a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index e9f7917385..87be1105b7 100644 --- a/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -332,7 +332,11 @@ export const useActions = () => { return storeWatcher; } - function trackActionSelected(action: IUpdateInformation, telemetry: Telemetry, rootView: string) { + function trackActionSelected( + action: IUpdateInformation, + _telemetry: Telemetry, + rootView: string, + ) { const payload = { node_type: action.key, action: action.name, @@ -340,7 +344,7 @@ export const useActions = () => { resource: (action.value as INodeParameters).resource || '', }; void useExternalHooks().run('nodeCreateList.addAction', payload); - telemetry?.trackNodesPanel('nodeCreateList.addAction', payload); + useNodeCreatorStore().onAddActions(payload); } return { diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index 70e3f048c3..27dccaf305 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -84,7 +84,6 @@ import type { INodeTypeDescription, INodeTypeNameVersion, IPinData, - ITelemetryTrackProperties, IWorkflowBase, NodeInputConnections, NodeParameterValueType, @@ -733,25 +732,22 @@ export function useCanvasOperations({ router }: { router: ReturnType { - return { - eventName: 'User entered nodes panel search term', - properties: { - search_string: nodesPanelSession.data.nodeFilter, - results_count: nodesPanelSession.data.resultsNodes.length, - results_nodes: nodesPanelSession.data.resultsNodes, - filter_mode: nodesPanelSession.data.filterMode, - nodes_panel_session_id: nodesPanelSession.pushRef, - }, - }; -}; - -export const hooksResetNodesPanelSession = () => { - nodesPanelSession.pushRef = `nodes_panel_session_${new Date().valueOf()}`; - nodesPanelSession.data = { - nodeFilter: '', - resultsNodes: [], - filterMode: 'Regular', - }; -}; diff --git a/packages/editor-ui/src/hooks/utils/index.ts b/packages/editor-ui/src/hooks/utils/index.ts index 43979c5141..75da3e6dfd 100644 --- a/packages/editor-ui/src/hooks/utils/index.ts +++ b/packages/editor-ui/src/hooks/utils/index.ts @@ -1,2 +1 @@ export * from './hooksAddFakeDoorFeatures'; -export * from './hooksNodesPanel'; diff --git a/packages/editor-ui/src/plugins/telemetry/index.ts b/packages/editor-ui/src/plugins/telemetry/index.ts index 7c14fc9933..56d17f0ad1 100644 --- a/packages/editor-ui/src/plugins/telemetry/index.ts +++ b/packages/editor-ui/src/plugins/telemetry/index.ts @@ -3,8 +3,8 @@ import type { ITelemetrySettings } from '@n8n/api-types'; import type { ITelemetryTrackProperties, IDataObject } from 'n8n-workflow'; import type { RouteLocation } from 'vue-router'; -import type { INodeCreateElement, IUpdateInformation } from '@/Interface'; -import type { IUserNodesPanelSession, RudderStack } from './telemetry.types'; +import type { IUpdateInformation } from '@/Interface'; +import type { RudderStack } from './telemetry.types'; import { APPEND_ATTRIBUTION_DEFAULT_PATH, MICROSOFT_TEAMS_NODE_TYPE, @@ -26,15 +26,6 @@ export class Telemetry { return window.rudderanalytics; } - private userNodesPanelSession: IUserNodesPanelSession = { - pushRef: '', - data: { - nodeFilter: '', - resultsNodes: [], - filterMode: 'Regular', - }, - }; - constructor() { this.pageEventQueue = []; this.previousPath = ''; @@ -200,78 +191,6 @@ export class Telemetry { } } - trackNodesPanel(event: string, properties: IDataObject = {}) { - if (this.rudderStack) { - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - switch (event) { - case 'nodeView.createNodeActiveChanged': - if (properties.createNodeActive !== false) { - this.resetNodesPanelSession(); - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - this.track('User opened nodes panel', properties); - } - break; - case 'nodeCreateList.destroyed': - if ( - this.userNodesPanelSession.data.nodeFilter.length > 0 && - this.userNodesPanelSession.data.nodeFilter !== '' - ) { - 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.track('User entered nodes panel search term', this.generateNodesPanelEvent()); - } - - if ( - (properties.newValue as string).length > ((properties.oldValue as string) || '').length - ) { - this.userNodesPanelSession.data.nodeFilter = properties.newValue as string; - this.userNodesPanelSession.data.resultsNodes = ( - (properties.filteredNodes || []) as INodeCreateElement[] - ).map((node: INodeCreateElement) => node.key); - } - break; - case 'nodeCreateList.onCategoryExpanded': - properties.is_subcategory = false; - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - this.track('User viewed node category', properties); - break; - case 'nodeCreateList.onViewActions': - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - this.track('User viewed node actions', properties); - break; - case 'nodeCreateList.onActionsCustmAPIClicked': - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - this.track('User clicked custom API from node actions', properties); - break; - case 'nodeCreateList.addAction': - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - this.track('User added action', properties); - break; - case 'nodeCreateList.onSubcategorySelected': - properties.category_name = properties.subcategory; - properties.is_subcategory = true; - properties.nodes_panel_session_id = this.userNodesPanelSession.pushRef; - delete properties.selected; - this.track('User viewed node category', properties); - break; - case 'nodeView.addNodeButton': - this.track('User added node to workflow canvas', properties, { withPostHog: true }); - break; - case 'nodeView.addSticky': - this.track('User inserted workflow note', properties); - break; - default: - break; - } - } - } - // We currently do not support tracking directly from within node implementation // so we are using this method as centralized way to track node parameters changes trackNodeParametersValuesChange(nodeType: string, change: IUpdateInformation) { @@ -295,24 +214,6 @@ export class Telemetry { } } - private resetNodesPanelSession() { - this.userNodesPanelSession.pushRef = `nodes_panel_session_${new Date().valueOf()}`; - this.userNodesPanelSession.data = { - nodeFilter: '', - resultsNodes: [], - filterMode: 'All', - }; - } - - private generateNodesPanelEvent() { - return { - search_string: this.userNodesPanelSession.data.nodeFilter, - results_count: this.userNodesPanelSession.data.resultsNodes.length, - filter_mode: this.userNodesPanelSession.data.filterMode, - nodes_panel_session_id: this.userNodesPanelSession.pushRef, - }; - } - private initRudderStack(key: string, url: string, options: IDataObject) { window.rudderanalytics = window.rudderanalytics || []; if (!this.rudderStack) { diff --git a/packages/editor-ui/src/stores/nodeCreator.store.test.ts b/packages/editor-ui/src/stores/nodeCreator.store.test.ts new file mode 100644 index 0000000000..b59d763e97 --- /dev/null +++ b/packages/editor-ui/src/stores/nodeCreator.store.test.ts @@ -0,0 +1,297 @@ +import { createPinia, setActivePinia } from 'pinia'; +import { useNodeCreatorStore } from './nodeCreator.store'; +import { useTelemetry } from '@/composables/useTelemetry'; +import { CUSTOM_API_CALL_KEY, REGULAR_NODE_CREATOR_VIEW } from '@/constants'; +import type { INodeCreateElement } from '@/Interface'; + +const workflow_id = 'workflow-id'; +const category_name = 'category-name'; +const source = 'source'; +const mode = 'mode'; +const now = 1717602004819; +const now1 = 1718602004819; +const node_type = 'node-type'; +const node_version = 1; +const input_node_type = 'input-node-type'; +const action = 'action'; +const source_mode = 'source-mode'; +const resource = 'resource'; +const actions = ['action1']; + +vi.mock('@/composables/useTelemetry', () => { + const track = vi.fn(); + return { + useTelemetry: () => { + return { + track, + }; + }, + }; +}); + +describe('useNodeCreatorStore', () => { + let nodeCreatorStore: ReturnType; + + beforeEach(() => { + vi.useFakeTimers(); + vi.restoreAllMocks(); + setActivePinia(createPinia()); + nodeCreatorStore = useNodeCreatorStore(); + vi.setSystemTime(now); + }); + + it('tracks when node creator is opened', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User opened nodes panel', + { + mode, + source, + nodes_panel_session_id: getSessionId(now), + workflow_id, + }, + { + withPostHog: false, + }, + ); + }); + + it('resets session id every time node creator is opened', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User opened nodes panel', + { + mode, + source, + nodes_panel_session_id: getSessionId(now), + workflow_id, + }, + { + withPostHog: false, + }, + ); + + vi.setSystemTime(now1); + + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User opened nodes panel', + { + mode, + source, + nodes_panel_session_id: getSessionId(now1), + workflow_id, + }, + { + withPostHog: false, + }, + ); + }); + + it('tracks event on category expanded', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onCategoryExpanded({ workflow_id, category_name }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User viewed node category', + { + category_name, + is_subcategory: false, + nodes_panel_session_id: getSessionId(now), + workflow_id, + }, + { + withPostHog: false, + }, + ); + }); + + it('tracks event when node is added to canvas', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onNodeAddedToCanvas({ + node_type, + node_version, + is_auto_add: true, + workflow_id, + drag_and_drop: true, + input_node_type, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User added node to workflow canvas', + { + node_type, + node_version, + is_auto_add: true, + drag_and_drop: true, + input_node_type, + nodes_panel_session_id: getSessionId(now), + workflow_id, + }, + { + withPostHog: true, + }, + ); + }); + + it('tracks event when action is added', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onAddActions({ + node_type, + action, + source_mode, + resource, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User added action', + { + node_type, + action, + source_mode, + resource, + nodes_panel_session_id: getSessionId(now), + }, + { + withPostHog: false, + }, + ); + }); + + it('tracks when custom api action is clicked', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onActionsCustomAPIClicked({ + app_identifier: node_type, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User clicked custom API from node actions', + { + app_identifier: node_type, + nodes_panel_session_id: getSessionId(now), + }, + { + withPostHog: false, + }, + ); + }); + + it('tracks when action is viewed', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onViewActions({ + app_identifier: node_type, + actions, + regular_action_count: 1, + trigger_action_count: 2, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User viewed node actions', + { + app_identifier: node_type, + actions, + regular_action_count: 1, + trigger_action_count: 2, + nodes_panel_session_id: getSessionId(now), + }, + { + withPostHog: false, + }, + ); + }); + + it('tracks when search filter is updated, ignoring custom actions in count', () => { + const newValue = 'new-value'; + const subcategory = 'subcategory'; + const title = 'title'; + + const mockTrigger = { + key: 'n8n-node.exampleTrigger', + properties: { + name: 'n8n-node.exampleTrigge', + displayName: 'Example Trigger', + }, + } as INodeCreateElement; + + const mockCustom = { + key: 'action', + properties: { + actionKey: CUSTOM_API_CALL_KEY, + }, + } as INodeCreateElement; + + const mockRegular = { + key: 'n8n-node.example', + properties: {}, + } as INodeCreateElement; + + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onNodeFilterChanged({ + newValue, + filteredNodes: [mockCustom, mockRegular, mockTrigger], + filterMode: REGULAR_NODE_CREATOR_VIEW, + subcategory, + title, + }); + + expect(useTelemetry().track).toHaveBeenCalledWith( + 'User entered nodes panel search term', + { + search_string: newValue, + filter_mode: 'regular', + category_name: subcategory, + results_count: 2, + trigger_count: 1, + regular_count: 1, + nodes_panel_session_id: getSessionId(now), + title, + }, + { + withPostHog: false, + }, + ); + }); +}); + +function getSessionId(time: number) { + return `nodes_panel_session_${time}`; +} diff --git a/packages/editor-ui/src/stores/nodeCreator.store.ts b/packages/editor-ui/src/stores/nodeCreator.store.ts index db6ce26eaa..05dd4a9795 100644 --- a/packages/editor-ui/src/stores/nodeCreator.store.ts +++ b/packages/editor-ui/src/stores/nodeCreator.store.ts @@ -1,6 +1,8 @@ import { defineStore } from 'pinia'; import { AI_NODE_CREATOR_VIEW, + AI_OTHERS_NODE_CREATOR_VIEW, + CUSTOM_API_CALL_KEY, NODE_CREATOR_OPEN_SOURCES, REGULAR_NODE_CREATOR_VIEW, STORES, @@ -12,17 +14,17 @@ import type { SimplifiedNodeType, ActionsRecord, ToggleNodeCreatorOptions, + INodeCreateElement, } from '@/Interface'; import { computed, ref } from 'vue'; import { transformNodeType } from '@/components/Node/NodeCreator/utils'; -import type { INodeInputConfiguration } from 'n8n-workflow'; +import type { IDataObject, INodeInputConfiguration, NodeParameterValueType } from 'n8n-workflow'; import { NodeConnectionType, nodeConnectionTypes, NodeHelpers } from 'n8n-workflow'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useUIStore } from '@/stores/ui.store'; import { useNDVStore } from '@/stores/ndv.store'; import { useExternalHooks } from '@/composables/useExternalHooks'; -import { useTelemetry } from '@/composables/useTelemetry'; import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { @@ -33,15 +35,15 @@ import type { Connection } from '@vue-flow/core'; import { CanvasConnectionMode } from '@/types'; import { isVueFlowConnection } from '@/utils/typeGuards'; import type { PartialBy } from '@/utils/typeHelpers'; +import { useTelemetry } from '@/composables/useTelemetry'; export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { const workflowsStore = useWorkflowsStore(); const ndvStore = useNDVStore(); const uiStore = useUIStore(); const nodeTypesStore = useNodeTypesStore(); - - const externalHooks = useExternalHooks(); const telemetry = useTelemetry(); + const externalHooks = useExternalHooks(); const selectedView = ref(TRIGGER_NODE_CREATOR_VIEW); const mergedNodes = ref([]); @@ -50,6 +52,10 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { const showScrim = ref(false); const openSource = ref(''); + const isCreateNodeActive = ref(false); + + const nodePanelSessionId = ref(''); + const allNodeCreatorNodes = computed(() => Object.values(mergedNodes.value).map((i) => transformNodeType(i)), ); @@ -115,7 +121,7 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { createNodeActive, nodeCreatorView, }: ToggleNodeCreatorOptions) { - if (createNodeActive === uiStore.isCreateNodeActive) { + if (createNodeActive === isCreateNodeActive.value) { return; } @@ -128,54 +134,24 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { // Default to the trigger tab in node creator if there's no trigger node yet setSelectedView(nodeCreatorView); - let mode; - switch (selectedView.value) { - case AI_NODE_CREATOR_VIEW: - mode = 'ai'; - break; - case REGULAR_NODE_CREATOR_VIEW: - mode = 'regular'; - break; - default: - mode = 'regular'; - } - - uiStore.isCreateNodeActive = createNodeActive; + isCreateNodeActive.value = createNodeActive; if (createNodeActive && source) { setOpenSource(source); } void externalHooks.run('nodeView.createNodeActiveChanged', { source, - mode, + mode: getMode(nodeCreatorView), createNodeActive, }); - trackNodesPanelActiveChanged({ - source, - mode, - createNodeActive, - workflowId: workflowsStore.workflowId, - }); - } - - function trackNodesPanelActiveChanged({ - source, - mode, - createNodeActive, - workflowId, - }: { - source?: string; - mode?: string; - createNodeActive?: boolean; - workflowId?: string; - }) { - telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { - source, - mode, - createNodeActive, - workflow_id: workflowId, - }); + if (createNodeActive) { + onCreatorOpened({ + source, + mode: getMode(nodeCreatorView), + workflow_id: workflowsStore.workflowId, + }); + } } function openNodeCreatorForConnectingNode({ @@ -264,7 +240,150 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { return filter; } + function resetNodesPanelSession() { + nodePanelSessionId.value = `nodes_panel_session_${new Date().valueOf()}`; + } + + function trackNodeCreatorEvent(event: string, properties: IDataObject = {}, withPostHog = false) { + telemetry.track( + event, + { + ...properties, + nodes_panel_session_id: nodePanelSessionId.value, + }, + { + withPostHog, + }, + ); + } + + function onCreatorOpened({ + source, + mode, + workflow_id, + }: { + source?: string; + mode: string; + workflow_id?: string; + }) { + resetNodesPanelSession(); + trackNodeCreatorEvent('User opened nodes panel', { + source, + mode, + workflow_id, + }); + } + + function onNodeFilterChanged({ + newValue, + filteredNodes, + filterMode, + subcategory, + title, + }: { + newValue: string; + filteredNodes: INodeCreateElement[]; + filterMode: NodeFilterType; + subcategory?: string; + title?: string; + }) { + if (!newValue.length) { + return; + } + + const { results_count, trigger_count, regular_count } = filteredNodes.reduce( + (accu, node) => { + if (!('properties' in node)) { + return accu; + } + const isCustomAction = + 'actionKey' in node.properties && node.properties.actionKey === CUSTOM_API_CALL_KEY; + if (isCustomAction) { + return accu; + } + const isTrigger = node.key.includes('Trigger'); + return { + results_count: accu.results_count + 1, + trigger_count: accu.trigger_count + (isTrigger ? 1 : 0), + regular_count: accu.regular_count + (isTrigger ? 0 : 1), + }; + }, + { + results_count: 0, + trigger_count: 0, + regular_count: 0, + }, + ); + + trackNodeCreatorEvent('User entered nodes panel search term', { + search_string: newValue, + filter_mode: getMode(filterMode), + category_name: subcategory, + results_count, + trigger_count, + regular_count, + title, + }); + } + + function onCategoryExpanded(properties: { category_name: string; workflow_id: string }) { + trackNodeCreatorEvent('User viewed node category', { ...properties, is_subcategory: false }); + } + + function onViewActions(properties: { + app_identifier: string; + actions: string[]; + regular_action_count: number; + trigger_action_count: number; + }) { + trackNodeCreatorEvent('User viewed node actions', properties); + } + + function onActionsCustomAPIClicked(properties: { app_identifier: string }) { + trackNodeCreatorEvent('User clicked custom API from node actions', properties); + } + + function onAddActions(properties: { + node_type?: string; + action: string; + source_mode: string; + resource: NodeParameterValueType; + }) { + trackNodeCreatorEvent('User added action', properties); + } + + function onSubcategorySelected(properties: { subcategory: string }) { + trackNodeCreatorEvent('User viewed node category', { + category_name: properties.subcategory, + is_subcategory: true, + }); + } + + function onNodeAddedToCanvas(properties: { + node_type: string; + node_version: number; + is_auto_add?: boolean; + workflow_id: string; + drag_and_drop?: boolean; + input_node_type?: string; + }) { + trackNodeCreatorEvent('User added node to workflow canvas', properties, true); + } + + function getMode(mode: NodeFilterType): string { + if (mode === AI_NODE_CREATOR_VIEW || mode === AI_OTHERS_NODE_CREATOR_VIEW) { + return 'ai'; + } + + if (mode === TRIGGER_NODE_CREATOR_VIEW) { + return 'trigger'; + } + + return 'regular'; + } + return { + isCreateNodeActive, openSource, selectedView, showScrim, @@ -280,5 +399,13 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { openNodeCreatorForConnectingNode, openNodeCreatorForTriggerNodes, allNodeCreatorNodes, + onCreatorOpened, + onNodeFilterChanged, + onCategoryExpanded, + onActionsCustomAPIClicked, + onViewActions, + onAddActions, + onSubcategorySelected, + onNodeAddedToCanvas, }; }); diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts index 4121099291..8353762caf 100644 --- a/packages/editor-ui/src/stores/ui.store.ts +++ b/packages/editor-ui/src/stores/ui.store.ts @@ -192,7 +192,6 @@ export const useUIStore = defineStore(STORES.UI, () => { const bannersHeight = ref(0); const bannerStack = ref([]); const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({}); - const isCreateNodeActive = ref(false); const appGridWidth = ref(0); @@ -659,7 +658,6 @@ export const useUIStore = defineStore(STORES.UI, () => { nodeViewMoveInProgress, nodeViewInitialized, addFirstStepOnLoad, - isCreateNodeActive, sidebarMenuCollapsed, fakeDoorFeatures, bannerStack, diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index 95dee594a7..9cd22e3ea3 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -1450,7 +1450,7 @@ function selectNodes(ids: string[]) { function onClickPane(position: CanvasNode['position']) { lastClickPosition.value = [position.x, position.y]; - uiStore.isCreateNodeActive = false; + nodeCreatorStore.isCreateNodeActive = false; setNodeSelected(); } @@ -1643,7 +1643,7 @@ onBeforeUnmount(() => {