mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(core): Show sub-node error on the logs pane. Open logs pane on sub-node error (#10248)
This commit is contained in:
parent
0faf46f4f8
commit
57d1c9a99e
|
@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
|
|||
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking ‘Test workflow’';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
|
||||
export const MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
|
||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||
export const CODE_NODE_NAME = 'Code';
|
||||
export const SET_NODE_NAME = 'Set';
|
||||
|
@ -57,6 +58,7 @@ export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
|
|||
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
|
||||
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
|
||||
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
|
||||
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
|
||||
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
|
||||
export const WEBHOOK_NODE_NAME = 'Webhook';
|
||||
|
||||
|
|
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
279
cypress/e2e/233-AI-switch-to-logs-on-error.cy.ts
Normal file
|
@ -0,0 +1,279 @@
|
|||
import type { ExecutionError } from 'n8n-workflow/src';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import {
|
||||
addLanguageModelNodeToParent,
|
||||
addMemoryNodeToParent,
|
||||
addNodeToCanvas,
|
||||
addToolNodeToParent,
|
||||
navigateToNewWorkflowPage,
|
||||
openNode,
|
||||
} from '../composables/workflow';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
} from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
closeManualChatModal,
|
||||
getManualChatMessages,
|
||||
getManualChatModalLogs,
|
||||
getManualChatModalLogsEntries,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
const ndv = new NDV();
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
|
||||
function createRunDataWithError(inputMessage: string) {
|
||||
return [
|
||||
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
|
||||
jsonData: {
|
||||
main: { input: inputMessage },
|
||||
},
|
||||
}),
|
||||
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
|
||||
jsonData: {
|
||||
ai_memory: {
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputOverride: {
|
||||
ai_memory: [
|
||||
[
|
||||
{
|
||||
json: {
|
||||
action: 'loadMemoryVariables',
|
||||
values: {
|
||||
input: inputMessage,
|
||||
system_message: 'You are a helpful assistant',
|
||||
formatting_instructions:
|
||||
'IMPORTANT: Always call `format_final_response` to format your final response!',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
error: {
|
||||
message: 'Internal error',
|
||||
timestamp: 1722591723244,
|
||||
name: 'NodeOperationError',
|
||||
description: 'Internal error',
|
||||
context: {},
|
||||
cause: {
|
||||
name: 'error',
|
||||
severity: 'FATAL',
|
||||
code: '3D000',
|
||||
file: 'postinit.c',
|
||||
line: '885',
|
||||
routine: 'InitPostgres',
|
||||
} as unknown as Error,
|
||||
} as ExecutionError,
|
||||
}),
|
||||
createMockNodeExecutionData(AGENT_NODE_NAME, {
|
||||
executionStatus: 'error',
|
||||
error: {
|
||||
level: 'error',
|
||||
tags: {
|
||||
packageName: 'workflow',
|
||||
},
|
||||
context: {},
|
||||
functionality: 'configuration-node',
|
||||
name: 'NodeOperationError',
|
||||
timestamp: 1722591723244,
|
||||
node: {
|
||||
parameters: {
|
||||
notice: '',
|
||||
sessionIdType: 'fromInput',
|
||||
tableName: 'n8n_chat_histories',
|
||||
},
|
||||
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
|
||||
name: 'Postgres Chat Memory',
|
||||
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
|
||||
typeVersion: 1,
|
||||
position: [1140, 500],
|
||||
credentials: {
|
||||
postgres: {
|
||||
id: 'RkyZetVpGsSfEAhQ',
|
||||
name: 'Postgres account',
|
||||
},
|
||||
},
|
||||
},
|
||||
messages: ['database "chat11" does not exist'],
|
||||
description: 'Internal error',
|
||||
message: 'Internal error',
|
||||
} as unknown as ExecutionError,
|
||||
metadata: {
|
||||
subRun: [
|
||||
{
|
||||
node: 'Postgres Chat Memory',
|
||||
runIndex: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function setupTestWorkflow(chatTrigger: boolean = false) {
|
||||
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
|
||||
if (chatTrigger) {
|
||||
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
|
||||
} else {
|
||||
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
|
||||
}
|
||||
|
||||
addNodeToCanvas(AGENT_NODE_NAME, true);
|
||||
|
||||
if (!chatTrigger) {
|
||||
// Remove chat trigger
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName(MANUAL_CHAT_TRIGGER_NODE_DISPLAY_NAME)
|
||||
.find('[data-test-id="delete-node-button"]')
|
||||
.click({ force: true });
|
||||
|
||||
// Set manual trigger to output standard pinned data
|
||||
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||
ndv.actions.editPinnedData();
|
||||
ndv.actions.savePinnedData();
|
||||
ndv.actions.close();
|
||||
}
|
||||
|
||||
// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
|
||||
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
ndv.getters.parameterInput('sessionIdType').click();
|
||||
getVisibleSelect().contains('Define below').click();
|
||||
ndv.getters.parameterInput('sessionKey').type('asdasd');
|
||||
|
||||
clickGetBackToCanvas();
|
||||
|
||||
addLanguageModelNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AGENT_NODE_NAME,
|
||||
true,
|
||||
);
|
||||
|
||||
clickCreateNewCredential();
|
||||
setCredentialValues({
|
||||
apiKey: 'sk_test_123',
|
||||
});
|
||||
clickGetBackToCanvas();
|
||||
|
||||
WorkflowPage.actions.zoomToFit();
|
||||
}
|
||||
|
||||
function checkMessages(inputMessage: string, outputMessage: string) {
|
||||
const messages = getManualChatMessages();
|
||||
messages.should('have.length', 2);
|
||||
messages.should('contain', inputMessage);
|
||||
messages.should('contain', outputMessage);
|
||||
|
||||
getManualChatModalLogs().should('exist');
|
||||
getManualChatModalLogsEntries()
|
||||
.should('have.length', 1)
|
||||
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
|
||||
}
|
||||
|
||||
describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
|
||||
beforeEach(() => {
|
||||
navigateToNewWorkflowPage();
|
||||
});
|
||||
|
||||
it('should open logs tab by default when there was an error', () => {
|
||||
setupTestWorkflow(true);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
clickExecuteNode();
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => sendManualChatMessage(inputMessage),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
checkMessages(inputMessage, '[ERROR: Internal error]');
|
||||
closeManualChatModal();
|
||||
|
||||
// Open the AI Agent node to see the logs
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
// Finally check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
|
||||
it('should switch to logs tab on error, when NDV is already opened', () => {
|
||||
setupTestWorkflow(false);
|
||||
|
||||
openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
runMockWorkflowExecution({
|
||||
trigger: () => clickExecuteNode(),
|
||||
runData: createRunDataWithError(inputMessage),
|
||||
lastNodeExecuted: AGENT_NODE_NAME,
|
||||
});
|
||||
|
||||
// Check that logs pane is opened by default
|
||||
ndv.getters.outputDataContainer().should('be.visible');
|
||||
|
||||
ndv.getters.aiOutputModeToggle().should('be.visible');
|
||||
ndv.getters
|
||||
.aiOutputModeToggle()
|
||||
.find('[role="radio"]')
|
||||
.should('have.length', 2)
|
||||
.eq(1)
|
||||
.should('have.attr', 'aria-checked', 'true');
|
||||
|
||||
ndv.getters
|
||||
.outputPanel()
|
||||
.findChildByTestId('node-error-message')
|
||||
.should('be.visible')
|
||||
.should('contain', 'Error in sub-node');
|
||||
});
|
||||
});
|
|
@ -24,6 +24,7 @@ export class NDV extends BasePage {
|
|||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller .cm-content'),
|
||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||
aiOutputModeToggle: () => cy.getByTestId('ai-output-mode-select'),
|
||||
nodeOutputHint: () => cy.getByTestId('ndv-output-run-node-hint'),
|
||||
savePinnedDataButton: () =>
|
||||
this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||
|
|
|
@ -6,6 +6,7 @@ import type {
|
|||
SupplyData,
|
||||
ExecutionError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||
import type { Sandbox } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
||||
import { getSandboxContext } from 'n8n-nodes-base/dist/nodes/Code/Sandbox';
|
||||
|
@ -208,7 +209,7 @@ export class ToolCode implements INodeType {
|
|||
try {
|
||||
response = await runFunction(query);
|
||||
} catch (error: unknown) {
|
||||
executionError = error as ExecutionError;
|
||||
executionError = new NodeOperationError(this.getNode(), error as ExecutionError);
|
||||
response = `There was an error: "${executionError.message}"`;
|
||||
}
|
||||
|
||||
|
@ -229,6 +230,7 @@ export class ToolCode implements INodeType {
|
|||
} else {
|
||||
void this.addOutputData(NodeConnectionType.AiTool, index, [[{ json: { response } }]]);
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -21,6 +21,7 @@ import type { BaseTextKey } from '@/plugins/i18n';
|
|||
|
||||
type Props = {
|
||||
error: NodeError | NodeApiError | NodeOperationError;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
@ -377,7 +378,7 @@ function copySuccess() {
|
|||
></div>
|
||||
</div>
|
||||
|
||||
<div class="node-error-view__info">
|
||||
<div v-if="!compact" class="node-error-view__info">
|
||||
<div class="node-error-view__info-header">
|
||||
<p class="node-error-view__info-title">
|
||||
{{ i18n.baseText('nodeErrorView.details.title') }}
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
<div :class="$style.titleSection">
|
||||
<template v-if="hasAiMetadata">
|
||||
<n8n-radio-buttons
|
||||
data-test-id="ai-output-mode-select"
|
||||
v-model="outputMode"
|
||||
:options="outputTypes"
|
||||
@update:model-value="onUpdateOutputMode"
|
||||
|
@ -83,6 +84,7 @@
|
|||
<template v-if="outputMode === 'logs' && node" #content>
|
||||
<RunDataAi :node="node" :run-index="runIndex" />
|
||||
</template>
|
||||
|
||||
<template #recovered-artificial-output-data>
|
||||
<div :class="$style.recoveredOutputData">
|
||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
||||
|
@ -101,8 +103,8 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import type { IRunData, IRunExecutionData, Workflow } from 'n8n-workflow';
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import type { IRunData, IRunExecutionData, NodeError, Workflow } from 'n8n-workflow';
|
||||
import RunData from './RunData.vue';
|
||||
import RunInfo from './RunInfo.vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
@ -183,7 +185,7 @@ const pinnedData = usePinnedData(activeNode, {
|
|||
|
||||
// Data
|
||||
|
||||
const outputMode = ref<OutputType>('regular');
|
||||
const outputMode = ref<OutputType>(OUTPUT_TYPE.REGULAR);
|
||||
const outputTypes = ref([
|
||||
{ label: i18n.baseText('ndv.output.outType.regular'), value: OUTPUT_TYPE.REGULAR },
|
||||
{ label: i18n.baseText('ndv.output.outType.logs'), value: OUTPUT_TYPE.LOGS },
|
||||
|
@ -213,6 +215,16 @@ const hasAiMetadata = computed(() => {
|
|||
return false;
|
||||
});
|
||||
|
||||
// Determine the initial output mode to logs if the node has an error and the logs are available
|
||||
const defaultOutputMode = computed<OutputType>(() => {
|
||||
const hasError =
|
||||
workflowRunData.value &&
|
||||
node.value &&
|
||||
(workflowRunData.value[node.value.name]?.[props.runIndex]?.error as NodeError);
|
||||
|
||||
return Boolean(hasError) && hasAiMetadata.value ? OUTPUT_TYPE.LOGS : OUTPUT_TYPE.REGULAR;
|
||||
});
|
||||
|
||||
const isNodeRunning = computed(() => {
|
||||
return !!node.value && workflowsStore.isNodeExecuting(node.value.name);
|
||||
});
|
||||
|
@ -351,6 +363,20 @@ const onUpdateOutputMode = (outputMode: OutputType) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Set the initial output mode when the component is mounted
|
||||
onMounted(() => {
|
||||
outputMode.value = defaultOutputMode.value;
|
||||
});
|
||||
|
||||
// In case the output panel was opened when the node has not run yet,
|
||||
// defaultOutputMode will be "regular" at the time of mounting.
|
||||
// This is why we need to watch the defaultOutputMode and change the outputMode to "logs" if the node has run and criteria are met.
|
||||
watch(defaultOutputMode, (newValue: OutputType, oldValue: OutputType) => {
|
||||
if (newValue === OUTPUT_TYPE.LOGS && oldValue === OUTPUT_TYPE.REGULAR && hasNodeRun.value) {
|
||||
outputMode.value = defaultOutputMode.value;
|
||||
}
|
||||
});
|
||||
|
||||
const activatePane = () => {
|
||||
emit('activatePane');
|
||||
};
|
||||
|
|
|
@ -281,7 +281,15 @@
|
|||
})
|
||||
}}
|
||||
</n8n-text>
|
||||
<slot v-else-if="$slots['content']" name="content"></slot>
|
||||
<div v-else-if="$slots['content']">
|
||||
<NodeErrorView
|
||||
v-if="workflowRunErrorAsNodeError"
|
||||
:error="workflowRunErrorAsNodeError"
|
||||
:class="$style.inlineError"
|
||||
compact
|
||||
/>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
<NodeErrorView
|
||||
v-else-if="workflowRunErrorAsNodeError"
|
||||
:error="workflowRunErrorAsNodeError"
|
||||
|
@ -1147,7 +1155,7 @@ export default defineComponent({
|
|||
const error = this.workflowRunData?.[this.node.name]?.[this.runIndex]?.error;
|
||||
const errorsToTrack = ['unknown error'];
|
||||
|
||||
if (error && errorsToTrack.some((e) => error.message.toLowerCase().includes(e))) {
|
||||
if (error && errorsToTrack.some((e) => error.message?.toLowerCase().includes(e))) {
|
||||
this.$telemetry.track(
|
||||
`User encountered an error: "${error.message}"`,
|
||||
{
|
||||
|
@ -1776,6 +1784,13 @@ export default defineComponent({
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.inlineError {
|
||||
line-height: var(--font-line-height-xloose);
|
||||
padding-left: var(--spacing-s);
|
||||
padding-right: var(--spacing-s);
|
||||
padding-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.outputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
Loading…
Reference in a new issue