fix(editor): Fix AI Node Logs View to Filter Duplicate Executions (#15049)

Co-authored-by: autologie <suguru@n8n.io>
This commit is contained in:
oleg
2025-05-07 08:55:58 +02:00
committed by GitHub
parent 51190255c8
commit 86807978c1
7 changed files with 728 additions and 11 deletions

View File

@@ -80,13 +80,31 @@ function getTreeNodeDataRec(
return resultData.map((d) => createNode(parent, nodeName, currentDepth, d.runIndex, d));
}
// When at root depth, filter AI data to only show executions that were triggered by this node
// This prevents duplicate entries in logs when a sub-node is connected to multiple root nodes
// Nodes without source info or with empty source arrays are always included
const filteredAiData =
currentDepth === 0
? aiData?.filter(({ data }) => {
if (!data?.source || data.source.length === 0) {
return true;
}
return data.source.some(
(source) =>
source?.previousNode === nodeName &&
(runIndex === undefined || source.previousNodeRun === runIndex),
);
})
: aiData;
// Get the first level of children
const connectedSubNodes = workflow.getParentNodes(nodeName, 'ALL_NON_MAIN', 1);
const treeNode = createNode(parent, nodeName, currentDepth, runIndex ?? 0);
// Only include sub-nodes which have data
const children = (aiData ?? []).flatMap((data) =>
const children = (filteredAiData ?? []).flatMap((data) =>
connectedSubNodes.includes(data.node) && (runIndex === undefined || data.runIndex === runIndex)
? getTreeNodeDataRec(treeNode, data.node, currentDepth + 1, workflow, aiData, data.runIndex)
: [],
@@ -152,6 +170,9 @@ export function getReferencedData(
data: data[type][0],
inOut,
type: type as NodeConnectionType,
// Include source information in AI content to track which node triggered the execution
// This enables filtering in the UI to show only relevant executions
source: taskData.source,
metadata: {
executionTime: taskData.executionTime,
startTime: taskData.startTime,
@@ -310,10 +331,23 @@ function getTreeNodeDataRecV2(
// Get the first level of children
const connectedSubNodes = workflow.getParentNodes(node.name, 'ALL_NON_MAIN', 1);
const treeNode = createNodeV2(parent, node, currentDepth, runIndex ?? 0, runData);
const children = connectedSubNodes
.flatMap((subNodeName) =>
(data[subNodeName] ?? []).flatMap((t, index) => {
if (runIndex !== undefined && index !== runIndex) {
// At root depth, filter out node executions that weren't triggered by this node
// This prevents showing duplicate executions when a sub-node is connected to multiple parents
// Only filter nodes that have source information with valid previousNode references
const isMatched =
currentDepth === 0 && t.source?.length > 0
? t.source.some(
(source) =>
source?.previousNode === node.name &&
(runIndex === undefined || source.previousNodeRun === runIndex),
)
: runIndex === undefined || index === runIndex;
if (!isMatched) {
return [];
}
@@ -387,7 +421,12 @@ export function createLogEntries(workflow: Workflow, runData: IRunData) {
workflow.getChildNodes(nodeName, 'ALL_NON_MAIN').length > 0 ||
workflow.getNode(nodeName)?.disabled
? [] // skip sub nodes and disabled nodes
: taskData.map((task, runIndex) => ({ nodeName, task, runIndex })),
: taskData.map((task, runIndex) => ({
nodeName,
task,
runIndex,
nodeHasMultipleRuns: taskData.length > 1,
})),
)
.sort((a, b) => {
if (a.task.executionIndex !== undefined && b.task.executionIndex !== undefined) {
@@ -399,13 +438,15 @@ export function createLogEntries(workflow: Workflow, runData: IRunData) {
: a.task.startTime - b.task.startTime;
});
return runs.flatMap(({ nodeName, runIndex, task }) => {
if (workflow.getParentNodes(nodeName, 'ALL_NON_MAIN').length > 0) {
return getTreeNodeDataV2(nodeName, task, workflow, runData, undefined);
}
return getTreeNodeDataV2(nodeName, task, workflow, runData, runIndex);
});
return runs.flatMap(({ nodeName, runIndex, task, nodeHasMultipleRuns }) =>
getTreeNodeDataV2(
nodeName,
task,
workflow,
runData,
nodeHasMultipleRuns ? runIndex : undefined,
),
);
}
export function includesLogEntry(log: LogEntry, logs: LogEntry[]): boolean {