From 3fcabd40b3ea0a311dc55f35e5c6f72c2f53efa7 Mon Sep 17 00:00:00 2001 From: Suguru Inoue Date: Mon, 16 Jun 2025 12:09:45 +0200 Subject: [PATCH] fix(editor): Cannot expand sub execution log if it finished with an error (#16236) Co-authored-by: Csaba Tuncsik --- packages/frontend/editor-ui/package.json | 2 +- .../logs/components/LogsOverviewRow.vue | 6 +-- .../logs/composables/useLogsExecutionData.ts | 18 ++++---- .../src/features/logs/logs.utils.test.ts | 41 +++++++++++++++++++ .../editor-ui/src/features/logs/logs.utils.ts | 28 +++++++++---- pnpm-workspace.yaml | 4 +- 6 files changed, 77 insertions(+), 22 deletions(-) diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index 55699793c2..e05875d955 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -31,7 +31,6 @@ "@codemirror/view": "^6.26.3", "@dagrejs/dagre": "^1.1.4", "@lezer/common": "1.1.0", - "@n8n/rest-api-client": "workspace:*", "@n8n/api-types": "workspace:*", "@n8n/chat": "workspace:*", "@n8n/codemirror-lang": "workspace:*", @@ -41,6 +40,7 @@ "@n8n/design-system": "workspace:*", "@n8n/i18n": "workspace:*", "@n8n/permissions": "workspace:*", + "@n8n/rest-api-client": "workspace:*", "@n8n/stores": "workspace:*", "@n8n/utils": "workspace:*", "@replit/codemirror-indentation-markers": "^6.5.3", diff --git a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewRow.vue b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewRow.vue index 281102b791..157e61c125 100644 --- a/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewRow.vue +++ b/packages/frontend/editor-ui/src/features/logs/components/LogsOverviewRow.vue @@ -8,7 +8,7 @@ import { useI18n } from '@n8n/i18n'; import { I18nT } from 'vue-i18n'; import { toDayMonth, toTime } from '@/utils/formatters/dateFormatter'; import LogsViewNodeName from '@/features/logs/components/LogsViewNodeName.vue'; -import { getSubtreeTotalConsumedTokens } from '@/features/logs/logs.utils'; +import { getSubtreeTotalConsumedTokens, hasSubExecution } from '@/features/logs/logs.utils'; import { useTimestamp } from '@vueuse/core'; import type { LatestNodeInfo, LogEntry } from '@/features/logs/logs.types'; @@ -70,9 +70,7 @@ const subtreeConsumedTokens = computed(() => props.shouldShowTokenCountColumn ? getSubtreeTotalConsumedTokens(props.data, false) : undefined, ); -const hasChildren = computed( - () => props.data.children.length > 0 || !!props.data.runData?.metadata?.subExecution, -); +const hasChildren = computed(() => props.data.children.length > 0 || hasSubExecution(props.data)); function isLastChild(level: number) { let parent = props.data.parent; diff --git a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts index d505ab2e64..9eef55fff1 100644 --- a/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts +++ b/packages/frontend/editor-ui/src/features/logs/composables/useLogsExecutionData.ts @@ -4,7 +4,12 @@ import { Workflow, type IRunExecutionData } from 'n8n-workflow'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useThrottleFn } from '@vueuse/core'; -import { createLogTree, deepToRaw, mergeStartData } from '@/features/logs/logs.utils'; +import { + createLogTree, + deepToRaw, + findSubExecutionLocator, + mergeStartData, +} from '@/features/logs/logs.utils'; import { parse } from 'flatted'; import { useToast } from '@/composables/useToast'; import type { LatestNodeInfo, LogEntry } from '../logs.types'; @@ -68,15 +73,14 @@ export function useLogsExecutionData() { } async function loadSubExecution(logEntry: LogEntry) { - const executionId = logEntry.runData?.metadata?.subExecution?.executionId; - const workflowId = logEntry.runData?.metadata?.subExecution?.workflowId; + const locator = findSubExecutionLocator(logEntry); - if (!execData.value?.data || !executionId || !workflowId) { + if (!execData.value?.data || locator === undefined) { return; } try { - const subExecution = await workflowsStore.fetchExecutionDataById(executionId); + const subExecution = await workflowsStore.fetchExecutionDataById(locator.executionId); const data = subExecution?.data ? (parse(subExecution.data as unknown as string) as IRunExecutionData) : undefined; @@ -85,8 +89,8 @@ export function useLogsExecutionData() { throw Error('Data is missing'); } - subWorkflowExecData.value[executionId] = data; - subWorkflows.value[workflowId] = new Workflow({ + subWorkflowExecData.value[locator.executionId] = data; + subWorkflows.value[locator.workflowId] = new Workflow({ ...subExecution.workflowData, nodeTypes: workflowsStore.getNodeTypes(), }); diff --git a/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts b/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts index 22e3b564c2..b1eb42a636 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.utils.test.ts @@ -9,6 +9,7 @@ import { createLogTree, deepToRaw, findSelectedLogEntry, + findSubExecutionLocator, getDefaultCollapsedEntries, getTreeNodeData, mergeStartData, @@ -1271,3 +1272,43 @@ describe(restoreChatHistory, () => { ]); }); }); + +describe(findSubExecutionLocator, () => { + it('should return undefined if given log entry has no related sub execution', () => { + const found = findSubExecutionLocator( + createTestLogEntry({ + runData: createTestTaskData({ + metadata: {}, + }), + }), + ); + + expect(found).toBe(undefined); + }); + + it('should find workflowId and executionId in metadata', () => { + const found = findSubExecutionLocator( + createTestLogEntry({ + runData: createTestTaskData({ + metadata: { subExecution: { workflowId: 'w0', executionId: 'e0' } }, + }), + }), + ); + + expect(found).toEqual({ workflowId: 'w0', executionId: 'e0' }); + }); + + it('should find workflowId and executionId in error object', () => { + const found = findSubExecutionLocator( + createTestLogEntry({ + runData: createTestTaskData({ + error: { + errorResponse: { workflowId: 'w1', executionId: 'e1' }, + } as unknown as ExecutionError, + }), + }), + ); + + expect(found).toEqual({ workflowId: 'w1', executionId: 'e1' }); + }); +}); diff --git a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts index bc54f57173..9945e15187 100644 --- a/packages/frontend/editor-ui/src/features/logs/logs.utils.ts +++ b/packages/frontend/editor-ui/src/features/logs/logs.utils.ts @@ -10,6 +10,8 @@ import { type Workflow, type INode, type ISourceData, + parseErrorMetadata, + type RelatedExecution, } from 'n8n-workflow'; import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types'; import { isProxy, isReactive, isRef, toRaw } from 'vue'; @@ -70,13 +72,13 @@ function getChildNodes( runIndex: number | undefined, context: LogTreeCreationContext, ) { - if (hasSubExecution(treeNode)) { - const workflowId = treeNode.runData?.metadata?.subExecution?.workflowId; - const executionId = treeNode.runData?.metadata?.subExecution?.executionId; - const workflow = workflowId ? context.workflows[workflowId] : undefined; - const subWorkflowRunData = executionId ? context.subWorkflowData[executionId] : undefined; + const subExecutionLocator = findSubExecutionLocator(treeNode); - if (!workflow || !subWorkflowRunData || !executionId) { + if (subExecutionLocator !== undefined) { + const workflow = context.workflows[subExecutionLocator.workflowId]; + const subWorkflowRunData = context.subWorkflowData[subExecutionLocator.executionId]; + + if (!workflow || !subWorkflowRunData) { return []; } @@ -85,7 +87,7 @@ function getChildNodes( parent: treeNode, depth: context.depth + 1, workflow, - executionId, + executionId: subExecutionLocator.executionId, data: subWorkflowRunData, }); } @@ -434,7 +436,17 @@ export function mergeStartData( } export function hasSubExecution(entry: LogEntry): boolean { - return !!entry.runData?.metadata?.subExecution; + return findSubExecutionLocator(entry) !== undefined; +} + +export function findSubExecutionLocator(entry: LogEntry): RelatedExecution | undefined { + const metadata = entry.runData?.metadata?.subExecution; + + if (metadata) { + return { workflowId: metadata.workflowId, executionId: metadata.executionId }; + } + + return parseErrorMetadata(entry.runData?.error)?.subExecution; } export function getDefaultCollapsedEntries(entries: LogEntry[]): Record { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9e9d370f6b..91ffe64465 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -38,7 +38,7 @@ catalog: xml2js: 0.6.2 xss: 1.0.15 zod: 3.24.1 - 'zod-to-json-schema': 3.23.3 + zod-to-json-schema: 3.23.3 '@langchain/core': 0.3.48 '@langchain/openai': 0.5.0 '@langchain/anthropic': 0.3.21 @@ -66,4 +66,4 @@ catalogs: vue-tsc: ^2.2.8 vue-markdown-render: ^2.2.1 highlight.js: ^11.8.0 - 'element-plus': 2.4.3 + element-plus: 2.4.3