Skip to content

Commit

Permalink
feat: Add tracking for node errors and update node graph (#11060)
Browse files Browse the repository at this point in the history
  • Loading branch information
mutdmour authored Oct 15, 2024
1 parent ecbe568 commit d3b05f1
Show file tree
Hide file tree
Showing 10 changed files with 676 additions and 5 deletions.
12 changes: 9 additions & 3 deletions packages/cli/src/events/relays/telemetry.event-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,9 @@ export class TelemetryEventRelay extends EventRelay {
}

if (telemetryProperties.is_manual) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes, {
runData: runData.data.resultData?.runData,
});
telemetryProperties.node_graph = nodeGraphResult.nodeGraph;
telemetryProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);

Expand All @@ -663,7 +665,9 @@ export class TelemetryEventRelay extends EventRelay {

if (telemetryProperties.is_manual) {
if (!nodeGraphResult) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes, {
runData: runData.data.resultData?.runData,
});
}

let userRole: 'owner' | 'sharee' | undefined = undefined;
Expand All @@ -688,7 +692,9 @@ export class TelemetryEventRelay extends EventRelay {
};

if (!manualExecEventProperties.node_graph_string) {
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes, {
runData: runData.data.resultData?.runData,
});
manualExecEventProperties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('vue-router', () => ({
fullPath: vi.fn(),
}),
RouterLink: vi.fn(),
useRouter: vi.fn(),
}));

let route: ReturnType<typeof useRoute>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ vi.mock('vue-router', () => ({
params: {},
}),
RouterLink: vi.fn(),
useRouter: vi.fn(),
}));

const initialState = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ vi.mock('vue-router', async (importOriginal) => {
useRoute: () => ({
params: {},
}),
useRouter: vi.fn(),
};
});

Expand Down
206 changes: 204 additions & 2 deletions packages/editor-ui/src/stores/workflows.store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,31 @@ import {
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { ExecutionSummary, IConnection, INodeExecutionData } from 'n8n-workflow';
import { type ExecutionSummary, type IConnection, type INodeExecutionData } from 'n8n-workflow';
import { stringSizeInBytes } from '@/utils/typesUtils';
import { dataPinningEventBus } from '@/event-bus';
import { useUIStore } from '@/stores/ui.store';
import type { PushPayload } from '@n8n/api-types';
import { flushPromises } from '@vue/test-utils';

vi.mock('@/api/workflows', () => ({
getWorkflows: vi.fn(),
getWorkflow: vi.fn(),
getNewWorkflow: vi.fn(),
}));

const getNodeType = vi.fn();
vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({
getNodeType: vi.fn(),
getNodeType,
})),
}));

const track = vi.fn();
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({ track }),
}));

describe('useWorkflowsStore', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let uiStore: ReturnType<typeof useUIStore>;
Expand All @@ -33,6 +41,7 @@ describe('useWorkflowsStore', () => {
setActivePinia(createPinia());
workflowsStore = useWorkflowsStore();
uiStore = useUIStore();
track.mockReset();
});

it('should initialize with default state', () => {
Expand Down Expand Up @@ -441,4 +450,197 @@ describe('useWorkflowsStore', () => {
expect(uiStore.stateIsDirty).toBe(true);
});
});

describe('addNodeExecutionData', () => {
const { successEvent, errorEvent, executionReponse } = generateMockExecutionEvents();
it('should throw error if not initalized', () => {
expect(() => workflowsStore.addNodeExecutionData(successEvent)).toThrowError();
});

it('should add node success run data', () => {
workflowsStore.setWorkflowExecutionData(executionReponse);

// ACT
workflowsStore.addNodeExecutionData(successEvent);

expect(workflowsStore.workflowExecutionData).toEqual({
...executionReponse,
data: {
resultData: {
runData: {
[successEvent.nodeName]: [successEvent.data],
},
},
},
});
});

it('should add node error event and track errored executions', async () => {
workflowsStore.setWorkflowExecutionData(executionReponse);
workflowsStore.addNode({
parameters: {},
id: '554c7ff4-7ee2-407c-8931-e34234c5056a',
name: 'Edit Fields',
type: 'n8n-nodes-base.set',
position: [680, 180],
typeVersion: 3.4,
});

getNodeType.mockReturnValue(getMockEditFieldsNode());

// ACT
workflowsStore.addNodeExecutionData(errorEvent);
await flushPromises();

expect(workflowsStore.workflowExecutionData).toEqual({
...executionReponse,
data: {
resultData: {
runData: {
[errorEvent.nodeName]: [errorEvent.data],
},
},
},
});
expect(track).toHaveBeenCalledWith(
'Manual exec errored',
{
error_title: 'invalid syntax',
node_type: 'n8n-nodes-base.set',
node_type_version: 3.4,
node_id: '554c7ff4-7ee2-407c-8931-e34234c5056a',
node_graph_string:
'{"node_types":["n8n-nodes-base.set"],"node_connections":[],"nodes":{"0":{"id":"554c7ff4-7ee2-407c-8931-e34234c5056a","type":"n8n-nodes-base.set","version":3.4,"position":[680,180]}},"notes":{},"is_pinned":false}',
},
{
withPostHog: true,
},
);
});
});
});

function getMockEditFieldsNode() {
return {
displayName: 'Edit Fields (Set)',
name: 'n8n-nodes-base.set',
icon: 'fa:pen',
group: ['input'],
description: 'Modify, add, or remove item fields',
defaultVersion: 3.4,
iconColor: 'blue',
version: [3, 3.1, 3.2, 3.3, 3.4],
subtitle: '={{$parameter["mode"]}}',
defaults: {
name: 'Edit Fields',
},
inputs: ['main'],
outputs: ['main'],
properties: [],
};
}

function generateMockExecutionEvents() {
const executionReponse: IExecutionResponse = {
id: '1',
workflowData: {
id: '1',
name: '',
createdAt: '1',
updatedAt: '1',
nodes: [],
connections: {},
active: false,
versionId: '1',
},
finished: false,
mode: 'cli',
startedAt: new Date(),
status: 'new',
data: {
resultData: {
runData: {},
},
},
};
const successEvent: PushPayload<'nodeExecuteAfter'> = {
executionId: '59',
nodeName: 'When clicking ‘Test workflow’',
data: {
hints: [],
startTime: 1727867966633,
executionTime: 1,
source: [],
executionStatus: 'success',
data: {
main: [
[
{
json: {},
pairedItem: {
item: 0,
},
},
],
],
},
},
};

const errorEvent: PushPayload<'nodeExecuteAfter'> = {
executionId: '61',
nodeName: 'Edit Fields',
data: {
hints: [],
startTime: 1727869043441,
executionTime: 2,
source: [
{
previousNode: 'When clicking ‘Test workflow’',
},
],
executionStatus: 'error',
// @ts-expect-error simpler data type, not BE class with methods
error: {
level: 'error',
tags: {
packageName: 'workflow',
},
context: {
itemIndex: 0,
},
functionality: 'regular',
name: 'NodeOperationError',
timestamp: 1727869043442,
node: {
parameters: {
mode: 'manual',
duplicateItem: false,
assignments: {
assignments: [
{
id: '87afdb19-4056-4551-93ef-d0126a34eb83',
name: "={{ $('Wh }}",
value: '',
type: 'string',
},
],
},
includeOtherFields: false,
options: {},
},
id: '9fb34d2d-7191-48de-8f18-91a6a28d0230',
name: 'Edit Fields',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [1120, 180],
},
messages: [],
message: 'invalid syntax',
stack: 'NodeOperationError: invalid syntax',
},
},
};

return { executionReponse, errorEvent, successEvent };
}
38 changes: 38 additions & 0 deletions packages/editor-ui/src/stores/workflows.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ import { useProjectsStore } from '@/stores/projects.store';
import type { ProjectSharingData } from '@/types/projects.types';
import type { PushPayload } from '@n8n/api-types';
import { useLocalStorage } from '@vueuse/core';
import { useTelemetry } from '@/composables/useTelemetry';
import { TelemetryHelpers } from 'n8n-workflow';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useRouter } from 'vue-router';
import { useSettingsStore } from './settings.store';

const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '',
Expand Down Expand Up @@ -107,6 +112,10 @@ let cachedWorkflow: Workflow | null = null;

export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const telemetry = useTelemetry();
const router = useRouter();
const workflowHelpers = useWorkflowHelpers({ router });
const settingsStore = useSettingsStore();
// -1 means the backend chooses the default
// 0 is the old flow
// 1 is the new flow
Expand Down Expand Up @@ -1188,6 +1197,33 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
}

async function trackNodeExecution(pushData: PushPayload<'nodeExecuteAfter'>): Promise<void> {
const nodeName = pushData.nodeName;

if (pushData.data.error) {
const node = getNodeByName(nodeName);
telemetry.track(
'Manual exec errored',
{
error_title: pushData.data.error.message,
node_type: node?.type,
node_type_version: node?.typeVersion,
node_id: node?.id,
node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph(
await workflowHelpers.getWorkflowDataToSave(),
workflowHelpers.getNodeTypes(),
{
isCloudDeployment: settingsStore.isCloudDeployment,
},
).nodeGraph,
),
},
{ withPostHog: true },
);
}
}

function addNodeExecutionData(pushData: PushPayload<'nodeExecuteAfter'>): void {
if (!workflowExecutionData.value?.data) {
throw new Error('The "workflowExecutionData" is not initialized!');
Expand All @@ -1209,6 +1245,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
};
}
workflowExecutionData.value.data!.resultData.runData[pushData.nodeName].push(pushData.data);

void trackNodeExecution(pushData);
}

function clearNodeExecutionData(nodeName: string): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ vi.mock('vue-router', () => {
push,
}),
RouterLink: vi.fn(),
useRoute: vi.fn(),
};
});

Expand Down
2 changes: 2 additions & 0 deletions packages/workflow/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2482,6 +2482,8 @@ export interface INodeGraphItem {
toolSettings?: IDataObject; //various langchain tool's settings
sql?: string; //merge node combineBySql, cloud only
workflow_id?: string; //@n8n/n8n-nodes-langchain.toolWorkflow and n8n-nodes-base.executeWorkflow
runs?: number;
items_total?: number;
}

export interface INodeNameIndex {
Expand Down
Loading

0 comments on commit d3b05f1

Please sign in to comment.