diff --git a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts index 1615b54725..2fc048fd84 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/__tests__/js-task-runner.test.ts @@ -1436,4 +1436,42 @@ describe('JsTaskRunner', () => { expect(Duration.fromObject({ hours: 1 }).maliciousKey).toBeUndefined(); }); }); + + describe('stack trace', () => { + it('should extract correct line number from user-defined function stack trace', async () => { + const runner = createRunnerWithOpts({}); + const taskId = '1'; + const task = newTaskState(taskId); + const taskSettings: JSExecSettings = { + code: 'function my_function() {\n null.map();\n}\nmy_function();\nreturn []', + nodeMode: 'runOnceForAllItems', + continueOnFail: false, + workflowMode: 'manual', + }; + runner.runningTasks.set(taskId, task); + + const sendSpy = jest.spyOn(runner.ws, 'send').mockImplementation(() => {}); + jest.spyOn(runner, 'sendOffers').mockImplementation(() => {}); + jest + .spyOn(runner, 'requestData') + .mockResolvedValue(newDataRequestResponse([wrapIntoJson({ a: 1 })])); + + await runner.receivedSettings(taskId, taskSettings); + + expect(sendSpy).toHaveBeenCalled(); + const calledWith = sendSpy.mock.calls[0][0] as string; + expect(typeof calledWith).toBe('string'); + const calledObject = JSON.parse(calledWith); + expect(calledObject).toEqual({ + type: 'runner:taskerror', + taskId, + error: { + stack: expect.any(String), + message: expect.stringContaining('Cannot read properties of null'), + description: 'TypeError', + lineNumber: 2, // from user-defined function + }, + }); + }); + }); }); diff --git a/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts b/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts index ef593d9589..19d47860a4 100644 --- a/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts +++ b/packages/@n8n/task-runner/src/js-task-runner/errors/execution-error.ts @@ -1,8 +1,6 @@ import type { ErrorLike } from './error-like'; import { SerializableError } from './serializable-error'; -const VM_WRAPPER_FN_NAME = 'VmCodeWrapper'; - export class ExecutionError extends SerializableError { description: string | null = null; @@ -42,8 +40,7 @@ export class ExecutionError extends SerializableError { } const messageRow = stackRows.find((line) => line.includes('Error:')); - const lineNumberRow = stackRows.find((line) => line.includes(`at ${VM_WRAPPER_FN_NAME} `)); - const lineNumberDisplay = this.toLineNumberDisplay(lineNumberRow); + const lineNumberDisplay = this.toLineNumberDisplay(stackRows); if (!messageRow) { this.message = `Unknown error ${lineNumberDisplay}`; @@ -62,26 +59,34 @@ export class ExecutionError extends SerializableError { this.message = `${errorDetails} ${lineNumberDisplay}`; } - private toLineNumberDisplay(lineNumberRow?: string) { - if (!lineNumberRow) return ''; + private toLineNumberDisplay(stackRows: string[]) { + if (!stackRows || stackRows.length === 0) return ''; - // TODO: This doesn't work if there is a function definition in the code - // and the error is thrown from that function. - - const regex = new RegExp( - `at ${VM_WRAPPER_FN_NAME} \\(evalmachine\\.:(?\\d+):`, + const userFnLine = stackRows.find( + (row) => row.match(/\(evalmachine\.:\d+:\d+\)/) && !row.includes('VmCodeWrapper'), ); - const errorLineNumberMatch = lineNumberRow.match(regex); - if (!errorLineNumberMatch?.groups?.lineNumber) return null; - const lineNumber = errorLineNumberMatch.groups.lineNumber; - if (!lineNumber) return ''; + if (userFnLine) { + const match = userFnLine.match(/evalmachine\.:(\d+):/); + if (match) this.lineNumber = Number(match[1]); + } - this.lineNumber = Number(lineNumber); + if (this.lineNumber === undefined) { + const topLevelLine = stackRows.find( + (row) => row.includes('VmCodeWrapper') && row.includes('evalmachine.'), + ); + + if (topLevelLine) { + const match = topLevelLine.match(/evalmachine\.:(\d+):/); + if (match) this.lineNumber = Number(match[1]); + } + } + + if (this.lineNumber === undefined) return ''; return this.itemIndex === undefined - ? `[line ${lineNumber}]` - : `[line ${lineNumber}, for item ${this.itemIndex}]`; + ? `[line ${this.lineNumber}]` + : `[line ${this.lineNumber}, for item ${this.itemIndex}]`; } private toErrorDetailsAndType(messageRow?: string) {