diff --git a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts index 4448dbc41e..9b4d8aecd2 100644 --- a/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts +++ b/packages/cli/src/events/__tests__/telemetry-event-relay.test.ts @@ -1,7 +1,8 @@ import type { GlobalConfig } from '@n8n/config'; import { mock } from 'jest-mock-extended'; import { InstanceSettings } from 'n8n-core'; -import type { IWorkflowBase } from 'n8n-workflow'; +import type { INode, INodesGraphResult } from 'n8n-workflow'; +import { NodeApiError, TelemetryHelpers, type IRun, type IWorkflowBase } from 'n8n-workflow'; import { N8N_VERSION } from '@/constants'; import type { WorkflowEntity } from '@/databases/entities/workflow-entity'; @@ -28,6 +29,9 @@ describe('TelemetryEventRelay', () => { mode: 'smtp', }, }, + diagnostics: { + enabled: true, + }, endpoints: { metrics: { enable: true, @@ -1106,4 +1110,393 @@ describe('TelemetryEventRelay', () => { }); }); }); + + describe('workflow post execute events', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockWorkflowBase = mock({ + id: 'workflow123', + name: 'Test Workflow', + active: true, + nodes: [ + { + id: 'node1', + name: 'Start', + type: 'n8n-nodes-base.start', + parameters: {}, + typeVersion: 1, + position: [100, 200], + }, + ], + connections: {}, + createdAt: new Date(), + updatedAt: new Date(), + staticData: {}, + settings: {}, + }); + + it('should not track when workflow has no id', async () => { + const event: RelayEventMap['workflow-post-execute'] = { + workflow: { ...mockWorkflowBase, id: '' }, + executionId: 'execution123', + userId: 'user123', + }; + + eventService.emit('workflow-post-execute', event); + + expect(telemetry.trackWorkflowExecution).not.toHaveBeenCalled(); + }); + + it('should not track when execution status is "waiting"', async () => { + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mockWorkflowBase, + executionId: 'execution123', + userId: 'user123', + runData: { + status: 'waiting', + data: { resultData: {} }, + } as IRun, + }; + + eventService.emit('workflow-post-execute', event); + + expect(telemetry.trackWorkflowExecution).not.toHaveBeenCalled(); + }); + + it('should track successful workflow execution', async () => { + const runData = mock({ + finished: true, + status: 'success', + mode: 'manual', + data: { resultData: {} }, + }); + + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mockWorkflowBase, + executionId: 'execution123', + userId: 'user123', + runData: runData as unknown as IRun, + }; + + eventService.emit('workflow-post-execute', event); + + await flushPromises(); + + expect(telemetry.trackWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + workflow_id: 'workflow123', + user_id: 'user123', + success: true, + is_manual: true, + execution_mode: 'manual', + }), + ); + }); + + it('should call telemetry.track when manual node execution finished', async () => { + sharedWorkflowRepository.findSharingRole.mockResolvedValue('workflow:editor'); + + const runData = { + status: 'error', + mode: 'manual', + data: { + startData: { + destinationNode: 'OpenAI', + runNodeFilter: ['OpenAI'], + }, + resultData: { + runData: {}, + lastNodeExecuted: 'OpenAI', + error: new NodeApiError( + { + id: '1', + typeVersion: 1, + name: 'Jira', + type: 'n8n-nodes-base.jira', + parameters: {}, + position: [100, 200], + }, + { + message: 'Error message', + description: 'Incorrect API key provided', + httpCode: '401', + stack: '', + }, + { + message: 'Error message', + description: 'Error description', + level: 'warning', + functionality: 'regular', + }, + ), + }, + }, + } as IRun; + + const nodeGraph: INodesGraphResult = { + nodeGraph: { node_types: [], node_connections: [], webhookNodeNames: [] }, + nameIndices: { + Jira: '1', + OpenAI: '1', + }, + } as unknown as INodesGraphResult; + + jest.spyOn(TelemetryHelpers, 'generateNodesGraph').mockImplementation(() => nodeGraph); + + jest + .spyOn(TelemetryHelpers, 'getNodeTypeForName') + .mockImplementation( + () => ({ type: 'n8n-nodes-base.jira', version: 1, name: 'Jira' }) as unknown as INode, + ); + + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mockWorkflowBase, + executionId: 'execution123', + userId: 'user123', + runData, + }; + + eventService.emit('workflow-post-execute', event); + + await flushPromises(); + + expect(telemetry.track).toHaveBeenCalledWith( + 'Manual node exec finished', + expect.objectContaining({ + webhook_domain: null, + user_id: 'user123', + workflow_id: 'workflow123', + status: 'error', + executionStatus: 'error', + sharing_role: 'sharee', + error_message: 'Error message', + error_node_type: 'n8n-nodes-base.jira', + error_node_id: '1', + node_id: '1', + node_type: 'n8n-nodes-base.jira', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + }), + ); + + expect(telemetry.trackWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + workflow_id: 'workflow123', + success: false, + is_manual: true, + execution_mode: 'manual', + version_cli: N8N_VERSION, + error_message: 'Error message', + error_node_type: 'n8n-nodes-base.jira', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + error_node_id: '1', + }), + ); + }); + + it('should call telemetry.track when manual node execution finished with canceled error message', async () => { + sharedWorkflowRepository.findSharingRole.mockResolvedValue('workflow:owner'); + + const runData = { + status: 'error', + mode: 'manual', + data: { + startData: { + destinationNode: 'OpenAI', + runNodeFilter: ['OpenAI'], + }, + resultData: { + runData: {}, + lastNodeExecuted: 'OpenAI', + error: new NodeApiError( + { + id: '1', + typeVersion: 1, + name: 'Jira', + type: 'n8n-nodes-base.jira', + parameters: {}, + position: [100, 200], + }, + { + message: 'Error message', + description: 'Incorrect API key provided', + httpCode: '401', + stack: '', + }, + { + message: 'Error message canceled', + description: 'Error description', + level: 'warning', + functionality: 'regular', + }, + ), + }, + }, + } as IRun; + + const nodeGraph: INodesGraphResult = { + nodeGraph: { node_types: [], node_connections: [] }, + nameIndices: { + Jira: '1', + OpenAI: '1', + }, + } as unknown as INodesGraphResult; + + jest.spyOn(TelemetryHelpers, 'generateNodesGraph').mockImplementation(() => nodeGraph); + + jest + .spyOn(TelemetryHelpers, 'getNodeTypeForName') + .mockImplementation( + () => ({ type: 'n8n-nodes-base.jira', version: 1, name: 'Jira' }) as unknown as INode, + ); + + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mockWorkflowBase, + executionId: 'execution123', + userId: 'user123', + runData, + }; + + eventService.emit('workflow-post-execute', event); + + await flushPromises(); + + expect(telemetry.track).toHaveBeenCalledWith( + 'Manual node exec finished', + expect.objectContaining({ + webhook_domain: null, + user_id: 'user123', + workflow_id: 'workflow123', + status: 'canceled', + executionStatus: 'canceled', + sharing_role: 'owner', + error_message: 'Error message canceled', + error_node_type: 'n8n-nodes-base.jira', + error_node_id: '1', + node_id: '1', + node_type: 'n8n-nodes-base.jira', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + }), + ); + + expect(telemetry.trackWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + workflow_id: 'workflow123', + success: false, + is_manual: true, + execution_mode: 'manual', + version_cli: N8N_VERSION, + error_message: 'Error message canceled', + error_node_type: 'n8n-nodes-base.jira', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + error_node_id: '1', + }), + ); + }); + + it('should call telemetry.track when manual workflow execution finished', async () => { + sharedWorkflowRepository.findSharingRole.mockResolvedValue('workflow:owner'); + + const runData = { + status: 'error', + mode: 'manual', + data: { + startData: { + runNodeFilter: ['OpenAI'], + }, + resultData: { + runData: { + Jira: [ + { + data: { main: [[{ json: { headers: { origin: 'https://www.test.com' } } }]] }, + }, + ], + }, + lastNodeExecuted: 'OpenAI', + error: new NodeApiError( + { + id: '1', + typeVersion: 1, + name: 'Jira', + type: 'n8n-nodes-base.jira', + parameters: {}, + position: [100, 200], + }, + { + message: 'Error message', + description: 'Incorrect API key provided', + httpCode: '401', + stack: '', + }, + { + message: 'Error message', + description: 'Error description', + level: 'warning', + functionality: 'regular', + }, + ), + }, + }, + } as unknown as IRun; + + const nodeGraph: INodesGraphResult = { + webhookNodeNames: ['Jira'], + nodeGraph: { node_types: [], node_connections: [] }, + nameIndices: { + Jira: '1', + OpenAI: '1', + }, + } as unknown as INodesGraphResult; + + jest.spyOn(TelemetryHelpers, 'generateNodesGraph').mockImplementation(() => nodeGraph); + + jest + .spyOn(TelemetryHelpers, 'getNodeTypeForName') + .mockImplementation( + () => ({ type: 'n8n-nodes-base.jira', version: 1, name: 'Jira' }) as unknown as INode, + ); + + const event: RelayEventMap['workflow-post-execute'] = { + workflow: mockWorkflowBase, + executionId: 'execution123', + userId: 'user123', + runData, + }; + + eventService.emit('workflow-post-execute', event); + + await flushPromises(); + + expect(telemetry.track).toHaveBeenCalledWith( + 'Manual workflow exec finished', + expect.objectContaining({ + webhook_domain: 'test.com', + user_id: 'user123', + workflow_id: 'workflow123', + status: 'error', + executionStatus: 'error', + sharing_role: 'owner', + error_message: 'Error message', + error_node_type: 'n8n-nodes-base.jira', + error_node_id: '1', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + }), + ); + + expect(telemetry.trackWorkflowExecution).toHaveBeenCalledWith( + expect.objectContaining({ + workflow_id: 'workflow123', + success: false, + is_manual: true, + execution_mode: 'manual', + version_cli: N8N_VERSION, + error_message: 'Error message', + error_node_type: 'n8n-nodes-base.jira', + node_graph_string: JSON.stringify(nodeGraph.nodeGraph), + error_node_id: '1', + }), + ); + }); + }); });