From e86edf536f647b2c215bcf8f289c7127d975e7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 6 May 2025 09:42:46 +0200 Subject: [PATCH] fix(core): Fix task runner logging to browser console (#15111) --- .../src/js-task-runner/js-task-runner.ts | 18 +++++++--- .../__tests__/execute-context.test.ts | 33 +++++++++++++++++++ .../node-execution-context/execute-context.ts | 6 +++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts index fa48fdf0cf..ddf7ba07bf 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/js-task-runner.ts @@ -1,7 +1,14 @@ +import { isObject } from 'lodash'; import set from 'lodash/set'; import { DateTime, Duration, Interval } from 'luxon'; import { getAdditionalKeys } from 'n8n-core'; -import { WorkflowDataProxy, Workflow, ObservableObject, Expression } from 'n8n-workflow'; +import { + WorkflowDataProxy, + Workflow, + ObservableObject, + Expression, + jsonStringify, +} from 'n8n-workflow'; import type { CodeExecutionMode, IWorkflowExecuteAdditionalData, @@ -19,7 +26,6 @@ import type { IWorkflowDataProxyData, } from 'n8n-workflow'; import * as a from 'node:assert'; -import { inspect } from 'node:util'; import { type Context, createContext, runInContext } from 'node:vm'; import type { MainConfig } from '@/config/main-config'; @@ -498,7 +504,11 @@ export class JsTaskRunner extends TaskRunner { // Send log output back to the main process. It will take care of forwarding // it to the UI or printing to console. log: (...args: unknown[]) => { - const formattedLogArgs = args.map((arg) => inspect(arg)); + const formattedLogArgs = args.map((arg) => { + if (isObject(arg) && '__isExecutionContext' in arg) return '[[ExecutionContext]]'; + if (typeof arg === 'string') return `'${arg}'`; + return jsonStringify(arg, { replaceCircularRefs: true }); + }); void this.makeRpcCall(taskId, 'logNodeOutput', formattedLogArgs); }, }; @@ -521,7 +531,7 @@ export class JsTaskRunner extends TaskRunner { additionalProperties: Record = {}, ): Context { return createContext({ - [inspect.custom]: () => '[[ExecutionContext]]', + __isExecutionContext: true, require: this.requireResolver, module: {}, console: this.buildCustomConsole(taskId), diff --git a/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts b/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts index 8ff738d1ef..7117e3e888 100644 --- a/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts +++ b/packages/core/src/execution-engine/node-execution-context/__tests__/execute-context.test.ts @@ -226,4 +226,37 @@ describe('ExecuteContext', () => { ]); }); }); + + describe('logNodeOutput', () => { + it('when in manual mode, should parse JSON', () => { + const json = '{"key": "value", "nested": {"foo": "bar"}}'; + const expectedParsedObject = { key: 'value', nested: { foo: 'bar' } }; + const numberArg = 42; + const stringArg = 'hello world!'; + + const manualModeContext = new ExecuteContext( + workflow, + node, + additionalData, + 'manual', + runExecutionData, + runIndex, + connectionInputData, + inputData, + executeData, + [closeFn], + abortSignal, + ); + + const sendMessageSpy = jest.spyOn(manualModeContext, 'sendMessageToUI'); + + manualModeContext.logNodeOutput(json, numberArg, stringArg); + + expect(sendMessageSpy.mock.calls[0][0]).toEqual(expectedParsedObject); + expect(sendMessageSpy.mock.calls[0][1]).toBe(numberArg); + expect(sendMessageSpy.mock.calls[0][2]).toBe(stringArg); + + sendMessageSpy.mockRestore(); + }); + }); }); diff --git a/packages/core/src/execution-engine/node-execution-context/execute-context.ts b/packages/core/src/execution-engine/node-execution-context/execute-context.ts index 0d4389b64b..ad7032f444 100644 --- a/packages/core/src/execution-engine/node-execution-context/execute-context.ts +++ b/packages/core/src/execution-engine/node-execution-context/execute-context.ts @@ -20,6 +20,7 @@ import { ApplicationError, createDeferredPromise, createEnvProviderState, + jsonParse, NodeConnectionTypes, } from 'n8n-workflow'; @@ -183,7 +184,10 @@ export class ExecuteContext extends BaseExecuteContext implements IExecuteFuncti logNodeOutput(...args: unknown[]): void { if (this.mode === 'manual') { - this.sendMessageToUI(...args); + const parsedLogArgs = args.map((arg) => + typeof arg === 'string' ? jsonParse(arg, { fallbackValue: arg }) : arg, + ); + this.sendMessageToUI(...parsedLogArgs); return; }