From 9b103af9355cf957abddd789c1554595be97c5d8 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Mon, 11 Aug 2025 09:04:20 +0200 Subject: [PATCH] feat(editor): Expand telemetry for "User added node to workflow canvas" event (#18150) --- packages/frontend/editor-ui/src/Interface.ts | 2 + .../NodeCreator/composables/useActions.ts | 1 + .../Node/NodeCreator/useActions.test.ts | 146 ++++++++++++++++++ .../composables/useCanvasOperations.test.ts | 67 ++++++++ .../src/composables/useCanvasOperations.ts | 14 +- .../src/stores/nodeCreator.store.test.ts | 28 ++++ .../editor-ui/src/stores/nodeCreator.store.ts | 3 + 7 files changed, 260 insertions(+), 1 deletion(-) diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index 70c1111f59..fa34c859c7 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -744,6 +744,7 @@ export type NodeTypeSelectedPayload = { resource?: string; operation?: string; }; + actionName?: string; }; export interface SubcategorizedNodeTypes { @@ -1230,6 +1231,7 @@ export type AddedNode = { type: string; openDetail?: boolean; isAutoAdd?: boolean; + actionName?: string; } & Partial; export type AddedNodeConnection = { diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts b/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts index 9a10679dbe..b8c6035048 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts @@ -182,6 +182,7 @@ export const useActions = () => { function actionDataToNodeTypeSelectedPayload(actionData: ActionData): NodeTypeSelectedPayload { const result: NodeTypeSelectedPayload = { type: actionData.key, + actionName: actionData.name, }; if ( diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/useActions.test.ts b/packages/frontend/editor-ui/src/components/Node/NodeCreator/useActions.test.ts index 386ff58c6e..5fd9e653b5 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/useActions.test.ts +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/useActions.test.ts @@ -273,4 +273,150 @@ describe('useActions', () => { }); }); }); + + describe('actionDataToNodeTypeSelectedPayload', () => { + test('should include actionName from ActionData', () => { + const { actionDataToNodeTypeSelectedPayload } = useActions(); + + const actionData = { + name: 'Create Contact', + key: 'hubspot', + value: { + resource: 'contact', + operation: 'create', + }, + }; + + const result = actionDataToNodeTypeSelectedPayload(actionData); + + expect(result).toEqual({ + type: 'hubspot', + actionName: 'Create Contact', + parameters: { + resource: 'contact', + operation: 'create', + }, + }); + }); + + test('should include actionName even when parameters are undefined', () => { + const { actionDataToNodeTypeSelectedPayload } = useActions(); + + const actionData = { + name: 'Send Message', + key: 'slack', + value: {}, + }; + + const result = actionDataToNodeTypeSelectedPayload(actionData); + + expect(result).toEqual({ + type: 'slack', + actionName: 'Send Message', + }); + }); + + test('should preserve existing resource and operation alongside actionName', () => { + const { actionDataToNodeTypeSelectedPayload } = useActions(); + + const actionData = { + name: 'Update Record', + key: 'airtable', + value: { + resource: 'base', + operation: 'update', + someOtherParam: 'value', + }, + }; + + const result = actionDataToNodeTypeSelectedPayload(actionData); + + expect(result).toEqual({ + type: 'airtable', + actionName: 'Update Record', + parameters: { + resource: 'base', + operation: 'update', + }, + }); + }); + }); + + describe('getAddedNodesAndConnections with actionName', () => { + test('should preserve actionName in nodes array', () => { + const workflowsStore = useWorkflowsStore(); + const nodeCreatorStore = useNodeCreatorStore(); + + vi.spyOn(workflowsStore, 'workflowTriggerNodes', 'get').mockReturnValue([]); + vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( + NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, + ); + vi.spyOn(nodeCreatorStore, 'selectedView', 'get').mockReturnValue(TRIGGER_NODE_CREATOR_VIEW); + + const { getAddedNodesAndConnections } = useActions(); + + const result = getAddedNodesAndConnections([ + { type: HTTP_REQUEST_NODE_TYPE, actionName: 'Make API Call' }, + ]); + + expect(result).toEqual({ + connections: [{ from: { nodeIndex: 0 }, to: { nodeIndex: 1 } }], + nodes: [ + { type: MANUAL_TRIGGER_NODE_TYPE, isAutoAdd: true }, + { type: HTTP_REQUEST_NODE_TYPE, openDetail: true, actionName: 'Make API Call' }, + ], + }); + }); + + test('should preserve actionName when no trigger is prepended', () => { + const workflowsStore = useWorkflowsStore(); + const nodeCreatorStore = useNodeCreatorStore(); + + vi.spyOn(workflowsStore, 'workflowTriggerNodes', 'get').mockReturnValue([ + { type: MANUAL_TRIGGER_NODE_TYPE } as never, + ]); + vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( + NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, + ); + vi.spyOn(nodeCreatorStore, 'selectedView', 'get').mockReturnValue(TRIGGER_NODE_CREATOR_VIEW); + + const { getAddedNodesAndConnections } = useActions(); + + const result = getAddedNodesAndConnections([ + { type: SLACK_NODE_TYPE, actionName: 'Post Message' }, + ]); + + expect(result).toEqual({ + connections: [], + nodes: [{ type: SLACK_NODE_TYPE, openDetail: true, actionName: 'Post Message' }], + }); + }); + + test('should work with multiple nodes having actionNames', () => { + const workflowsStore = useWorkflowsStore(); + const nodeCreatorStore = useNodeCreatorStore(); + + vi.spyOn(workflowsStore, 'workflowTriggerNodes', 'get').mockReturnValue([ + { type: MANUAL_TRIGGER_NODE_TYPE } as never, + ]); + vi.spyOn(nodeCreatorStore, 'openSource', 'get').mockReturnValue( + NODE_CREATOR_OPEN_SOURCES.ADD_NODE_BUTTON, + ); + + const { getAddedNodesAndConnections } = useActions(); + + const result = getAddedNodesAndConnections([ + { type: WEBHOOK_NODE_TYPE, openDetail: true, actionName: 'Receive Webhook' }, + { type: SLACK_NODE_TYPE, actionName: 'Send Notification' }, + ]); + + expect(result).toEqual({ + connections: [{ from: { nodeIndex: 0 }, to: { nodeIndex: 1 } }], + nodes: [ + { type: WEBHOOK_NODE_TYPE, openDetail: true, actionName: 'Receive Webhook' }, + { type: SLACK_NODE_TYPE, actionName: 'Send Notification' }, + ], + }); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts index 103812b8fc..651daa3e91 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.test.ts @@ -273,6 +273,38 @@ describe('useCanvasOperations', () => { await waitFor(() => expect(ndvStore.setActiveNodeName).not.toHaveBeenCalled()); }); + + it('should pass actionName to telemetry when adding node with action', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeCreatorStore = mockedStore(useNodeCreatorStore); + const nodeTypeDescription = mockNodeTypeDescription({ name: 'hubspot' }); + const actionName = 'Create Contact'; + + workflowsStore.addNode = vi.fn(); + nodeCreatorStore.onNodeAddedToCanvas = vi.fn(); + + const { addNode } = useCanvasOperations(); + addNode( + { + type: 'hubspot', + typeVersion: 1, + }, + nodeTypeDescription, + { + telemetry: true, + actionName, + }, + ); + + await waitFor(() => { + expect(nodeCreatorStore.onNodeAddedToCanvas).toHaveBeenCalledWith( + expect.objectContaining({ + action: actionName, + node_type: 'hubspot', + }), + ); + }); + }); }); describe('resolveNodePosition', () => { @@ -896,6 +928,41 @@ describe('useCanvasOperations', () => { expect(uiStore.stateIsDirty).toEqual(false); }); + + it('should pass actionName to telemetry when adding nodes with actions', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodeCreatorStore = mockedStore(useNodeCreatorStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeName = 'hubspot'; + const actionName = 'Create Contact'; + const nodes = [ + { + ...mockNode({ + name: 'HubSpot Node', + type: nodeTypeName, + position: [100, 100], + }), + actionName, + }, + ]; + + workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow); + nodeCreatorStore.onNodeAddedToCanvas = vi.fn(); + + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; + + const { addNodes } = useCanvasOperations(); + await addNodes(nodes, { telemetry: true }); + + expect(nodeCreatorStore.onNodeAddedToCanvas).toHaveBeenCalledWith( + expect.objectContaining({ + action: actionName, + node_type: nodeTypeName, + }), + ); + }); }); describe('revertAddNode', () => { diff --git a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts index 2b774b704e..0ffec15e53 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasOperations.ts @@ -139,6 +139,7 @@ type AddNodesOptions = AddNodesBaseOptions & { type AddNodeOptions = AddNodesBaseOptions & { openNDV?: boolean; isAutoAdd?: boolean; + actionName?: string; }; export function useCanvasOperations() { @@ -667,7 +668,7 @@ export function useCanvasOperations() { } for (const [index, nodeAddData] of nodesWithTypeVersion.entries()) { - const { isAutoAdd, openDetail: openNDV, ...node } = nodeAddData; + const { isAutoAdd, openDetail: openNDV, actionName, ...node } = nodeAddData; const position = node.position ?? insertPosition; const nodeTypeDescription = requireNodeTypeDescription(node.type, node.typeVersion); @@ -683,6 +684,7 @@ export function useCanvasOperations() { ...(index === 0 ? { viewport } : {}), openNDV, isAutoAdd, + actionName, }, ); lastAddedNode = newNode; @@ -904,6 +906,13 @@ export function useCanvasOperations() { } function trackAddDefaultNode(nodeData: INodeUi, options: AddNodeOptions) { + // Extract action-related parameters from node parameters if available + const nodeParameters = nodeData.parameters; + const resource = + typeof nodeParameters?.resource === 'string' ? nodeParameters.resource : undefined; + const operation = + typeof nodeParameters?.operation === 'string' ? nodeParameters.operation : undefined; + nodeCreatorStore.onNodeAddedToCanvas({ node_id: nodeData.id, node_type: nodeData.type, @@ -914,6 +923,9 @@ export function useCanvasOperations() { input_node_type: uiStore.lastInteractedWithNode ? uiStore.lastInteractedWithNode.type : undefined, + resource, + operation, + action: options.actionName, }); } diff --git a/packages/frontend/editor-ui/src/stores/nodeCreator.store.test.ts b/packages/frontend/editor-ui/src/stores/nodeCreator.store.test.ts index 5f0fedba3c..5e0d614611 100644 --- a/packages/frontend/editor-ui/src/stores/nodeCreator.store.test.ts +++ b/packages/frontend/editor-ui/src/stores/nodeCreator.store.test.ts @@ -334,6 +334,34 @@ describe('useNodeCreatorStore', () => { expect(nodeCreatorStore.selectedView).not.toEqual(REGULAR_NODE_CREATOR_VIEW); }); }); + + it('tracks when node is added to canvas with action parameter', () => { + nodeCreatorStore.onCreatorOpened({ + source, + mode, + workflow_id, + }); + nodeCreatorStore.onNodeAddedToCanvas({ + node_id, + node_type, + node_version, + workflow_id, + action: 'Create Contact', + resource: 'contact', + operation: 'create', + }); + + expect(useTelemetry().track).toHaveBeenCalledWith('User added node to workflow canvas', { + node_id, + node_type, + node_version, + workflow_id, + action: 'Create Contact', + resource: 'contact', + operation: 'create', + nodes_panel_session_id: getSessionId(now), + }); + }); }); function getSessionId(time: number) { diff --git a/packages/frontend/editor-ui/src/stores/nodeCreator.store.ts b/packages/frontend/editor-ui/src/stores/nodeCreator.store.ts index 9a4eb84887..f1b6e151fe 100644 --- a/packages/frontend/editor-ui/src/stores/nodeCreator.store.ts +++ b/packages/frontend/editor-ui/src/stores/nodeCreator.store.ts @@ -414,6 +414,9 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => { workflow_id: string; drag_and_drop?: boolean; input_node_type?: string; + resource?: string; + operation?: string; + action?: string; }) { trackNodeCreatorEvent('User added node to workflow canvas', properties); }