feat(API): Set up error tracking using Sentry (#4394)

* feat(cli): Setup error tracking using Sentry

* make error reporting available in the workflows package

* address some of the PR comments

* create a ErrorReporterProxy like LoggerProxy

* remove the `captureError` helper. use ErrorReporterProxy directly

* fix linting issues

* remove ErrorReporterProxy warnings in tests

* check for NODE_ENV === 'production' instead

* IErrorReporter -> ErrorReporter

* ErrorReporterProxy.getInstance() -> ErrorReporter

* allow capturing stacks in warnings as well

* make n8n debugging consistent with `npm start`

* IReportingOptions -> ReportingOptions

* use consistent signature for `error` and `warn`

* use Logger instead of console.log
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2022-11-04 17:34:47 +01:00 committed by GitHub
parent 0edd4bcc87
commit 41cb0eec6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 15097 additions and 8015 deletions

1
.vscode/launch.json vendored
View file

@ -14,6 +14,7 @@
{ {
"name": "Launch n8n with debug", "name": "Launch n8n with debug",
"program": "${workspaceFolder}/packages/cli/bin/n8n", "program": "${workspaceFolder}/packages/cli/bin/n8n",
"cwd": "${workspaceFolder}/packages/cli/bin",
"request": "launch", "request": "launch",
"skipFiles": ["<node_internals>/**"], "skipFiles": ["<node_internals>/**"],
"type": "node", "type": "node",

View file

@ -4,6 +4,7 @@ ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \ RUN \
apt-get update && \ apt-get update && \
apt-get -y install graphicsmagick gosu git apt-get -y install graphicsmagick gosu git

View file

@ -4,6 +4,7 @@ ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
RUN \ RUN \
yum install -y gcc-c++ make yum install -y gcc-c++ make

View file

@ -4,15 +4,16 @@ FROM n8nio/base:${NODE_VERSION}
ARG N8N_VERSION ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
ENV N8N_VERSION=${N8N_VERSION}
ENV NODE_ENV=production ENV NODE_ENV=production
RUN set -eux; \ RUN set -eux; \
apkArch="$(apk --print-arch)"; \ apkArch="$(apk --print-arch)"; \
case "$apkArch" in \ case "$apkArch" in \
'armv7') apk --no-cache add --virtual build-dependencies python3 build-base;; \ 'armv7') apk --no-cache add --virtual build-dependencies python3 build-base;; \
esac && \ esac && \
npm install -g --omit=dev n8n@${N8N_VERSION} && \ npm install -g --omit=dev n8n@${N8N_VERSION} && \
case "$apkArch" in \ case "$apkArch" in \
'armv7') apk del build-dependencies;; \ 'armv7') apk del build-dependencies;; \
esac && \ esac && \
find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm && \ find /usr/local/lib/node_modules/n8n -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" | xargs rm && \
rm -rf /root/.npm rm -rf /root/.npm

22881
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -948,6 +948,15 @@ export const schema = {
env: 'N8N_DIAGNOSTICS_POSTHOG_DISABLE_RECORDING', env: 'N8N_DIAGNOSTICS_POSTHOG_DISABLE_RECORDING',
}, },
}, },
sentry: {
dsn: {
doc: 'Data source name for error tracking on Sentry',
format: String,
default:
'https://1f954e089a054b8e943ae4f4042b2bff@o1420875.ingest.sentry.io/4504016528408576',
env: 'N8N_SENTRY_DSN',
},
},
frontend: { frontend: {
doc: 'Diagnostics config for frontend.', doc: 'Diagnostics config for frontend.',
format: String, format: String,

View file

@ -104,6 +104,8 @@
"@oclif/core": "^1.9.3", "@oclif/core": "^1.9.3",
"@oclif/errors": "^1.2.2", "@oclif/errors": "^1.2.2",
"@rudderstack/rudder-sdk-node": "1.0.6", "@rudderstack/rudder-sdk-node": "1.0.6",
"@sentry/node": "^7.17.3",
"@sentry/integrations": "^7.17.3",
"axios": "^0.21.1", "axios": "^0.21.1",
"basic-auth": "^2.0.1", "basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",

View file

@ -32,6 +32,7 @@ import {
WorkflowActivationError, WorkflowActivationError,
WorkflowExecuteMode, WorkflowExecuteMode,
LoggerProxy as Logger, LoggerProxy as Logger,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow'; } from 'n8n-workflow';
import express from 'express'; import express from 'express';
@ -121,6 +122,7 @@ export class ActiveWorkflowRunner {
}); });
console.log(` => Started`); console.log(` => Started`);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
console.log( console.log(
` => ERROR: Workflow could not be activated on first try, keep on trying`, ` => ERROR: Workflow could not be activated on first try, keep on trying`,
); );
@ -881,6 +883,7 @@ export class ActiveWorkflowRunner {
try { try {
await this.add(workflowId, activationMode, workflowData); await this.add(workflowId, activationMode, workflowData);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
let lastTimeout = this.queuedWorkflowActivations[workflowId].lastTimeout; let lastTimeout = this.queuedWorkflowActivations[workflowId].lastTimeout;
if (lastTimeout < WORKFLOW_REACTIVATE_MAX_TIMEOUT) { if (lastTimeout < WORKFLOW_REACTIVATE_MAX_TIMEOUT) {
lastTimeout = Math.min(lastTimeout * 2, WORKFLOW_REACTIVATE_MAX_TIMEOUT); lastTimeout = Math.min(lastTimeout * 2, WORKFLOW_REACTIVATE_MAX_TIMEOUT);
@ -948,6 +951,7 @@ export class ActiveWorkflowRunner {
try { try {
await this.removeWorkflowWebhooks(workflowId); await this.removeWorkflowWebhooks(workflowId);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
console.error( console.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`, `Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,

View file

@ -76,7 +76,7 @@ export const executeCommand = async (
command: string, command: string,
options?: { doNotHandleError?: boolean }, options?: { doNotHandleError?: boolean },
): Promise<string> => { ): Promise<string> => {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath(); const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const execOptions = { const execOptions = {
cwd: downloadFolder, cwd: downloadFolder,

View file

@ -38,6 +38,7 @@ import {
WorkflowExecuteMode, WorkflowExecuteMode,
ITaskDataConnections, ITaskDataConnections,
LoggerProxy as Logger, LoggerProxy as Logger,
ErrorReporterProxy as ErrorReporter,
IHttpRequestHelper, IHttpRequestHelper,
} from 'n8n-workflow'; } from 'n8n-workflow';
@ -672,6 +673,7 @@ export class CredentialsHelper extends ICredentialsHelper {
credentialsDecrypted, credentialsDecrypted,
); );
} catch (error) { } catch (error) {
ErrorReporter.error(error);
// Do not fail any requests to allow custom error messages and // Do not fail any requests to allow custom error messages and
// make logic easier // make logic easier
if (error.cause?.response) { if (error.cause?.response) {

View file

@ -0,0 +1,41 @@
import * as Sentry from '@sentry/node';
import { RewriteFrames } from '@sentry/integrations';
import type { Application } from 'express';
import config from '../config';
import { ErrorReporterProxy } from 'n8n-workflow';
let initialized = false;
export const initErrorHandling = (app?: Application) => {
if (initialized) return;
if (!config.getEnv('diagnostics.enabled')) {
initialized = true;
return;
}
const dsn = config.getEnv('diagnostics.config.sentry.dsn');
const { N8N_VERSION: release, ENVIRONMENT: environment } = process.env;
Sentry.init({
dsn,
release,
environment,
integrations: (integrations) => {
integrations.push(new RewriteFrames({ root: process.cwd() }));
return integrations;
},
});
if (app) {
const { requestHandler, errorHandler } = Sentry.Handlers;
app.use(requestHandler());
app.use(errorHandler());
}
ErrorReporterProxy.init({
report: (error, options) => Sentry.captureException(error, options),
});
initialized = true;
};

View file

@ -22,6 +22,7 @@ import {
IVersionedNodeType, IVersionedNodeType,
LoggerProxy, LoggerProxy,
jsonParse, jsonParse,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@ -125,19 +126,23 @@ class LoadNodesAndCredentialsClass {
const nodePackages = []; const nodePackages = [];
try { try {
// Read downloaded nodes and credentials // Read downloaded nodes and credentials
const downloadedNodesFolder = UserSettings.getUserN8nFolderDowloadedNodesPath(); const downloadedNodesFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules'); const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules');
await fsAccess(downloadedNodesFolderModules); await fsAccess(downloadedNodesFolderModules);
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules); const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules);
nodePackages.push(...downloadedPackages); nodePackages.push(...downloadedPackages);
// eslint-disable-next-line no-empty } catch (error) {
} catch (error) {} // Folder does not exist so ignore and return
return;
}
for (const packagePath of nodePackages) { for (const packagePath of nodePackages) {
try { try {
await this.loadDataFromPackage(packagePath); await this.loadDataFromPackage(packagePath);
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (error) {} } catch (error) {
ErrorReporter.error(error);
}
} }
} }
@ -231,7 +236,7 @@ class LoadNodesAndCredentialsClass {
} }
async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> { async loadNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath(); const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const command = `npm install ${packageName}${version ? `@${version}` : ''}`; const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
await executeCommand(command); await executeCommand(command);
@ -285,7 +290,7 @@ class LoadNodesAndCredentialsClass {
packageName: string, packageName: string,
installedPackage: InstalledPackages, installedPackage: InstalledPackages,
): Promise<InstalledPackages> { ): Promise<InstalledPackages> {
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath(); const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
const command = `npm i ${packageName}@latest`; const command = `npm i ${packageName}@latest`;

View file

@ -6,6 +6,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { parse, stringify } from 'flatted'; import { parse, stringify } from 'flatted';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { import {
@ -154,8 +155,12 @@ export function send<T, R extends Request, S extends Response>(
sendSuccessResponse(res, data, raw); sendSuccessResponse(res, data, raw);
} catch (error) { } catch (error) {
if (error instanceof Error && isUniqueConstraintError(error)) { if (error instanceof Error) {
error.message = 'There is already an entry with this name'; ErrorReporter.error(error);
if (isUniqueConstraintError(error)) {
error.message = 'There is already an entry with this name';
}
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument // eslint-disable-next-line @typescript-eslint/no-unsafe-argument

View file

@ -70,6 +70,7 @@ import {
jsonParse, jsonParse,
WebhookHttpMethod, WebhookHttpMethod,
WorkflowExecuteMode, WorkflowExecuteMode,
ErrorReporterProxy as ErrorReporter,
} from 'n8n-workflow'; } from 'n8n-workflow';
import basicAuth from 'basic-auth'; import basicAuth from 'basic-auth';
@ -82,6 +83,7 @@ import parseUrl from 'parseurl';
import promClient, { Registry } from 'prom-client'; import promClient, { Registry } from 'prom-client';
import history from 'connect-history-api-fallback'; import history from 'connect-history-api-fallback';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import config from '../config'; import config from '../config';
import * as Queue from './Queue'; import * as Queue from './Queue';
@ -153,6 +155,7 @@ import glob from 'fast-glob';
import { ResponseError } from './ResponseHelper'; import { ResponseError } from './ResponseHelper';
import { toHttpNodeParameters } from './CurlConverterHelper'; import { toHttpNodeParameters } from './CurlConverterHelper';
import { initErrorHandling } from './ErrorReporting';
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
@ -256,6 +259,8 @@ class App {
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint'); this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
initErrorHandling(this.app);
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
const telemetrySettings: ITelemetrySettings = { const telemetrySettings: ITelemetrySettings = {
enabled: config.getEnv('diagnostics.enabled'), enabled: config.getEnv('diagnostics.enabled'),
@ -742,6 +747,7 @@ class App {
// DB ping // DB ping
await connection.query('SELECT 1'); await connection.query('SELECT 1');
} catch (err) { } catch (err) {
ErrorReporter.error(err);
LoggerProxy.error('No Database connection!', err); LoggerProxy.error('No Database connection!', err);
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503); const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error); return ResponseHelper.sendErrorResponse(res, error);

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { createTransport, Transporter } from 'nodemailer'; import { createTransport, Transporter } from 'nodemailer';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
import * as config from '../../../config'; import * as config from '../../../config';
import { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces'; import { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces';
@ -24,22 +24,15 @@ export class NodeMailer implements UserManagementMailerImplementation {
const user = config.getEnv('userManagement.emails.smtp.auth.user'); const user = config.getEnv('userManagement.emails.smtp.auth.user');
const pass = config.getEnv('userManagement.emails.smtp.auth.pass'); const pass = config.getEnv('userManagement.emails.smtp.auth.pass');
return new Promise((resolve, reject) => { try {
this.transport.verify((error: Error) => { await this.transport.verify();
if (!error) { } catch (error) {
resolve(); const message: string[] = [];
return; if (!host) message.push('SMTP host not defined (N8N_SMTP_HOST).');
} if (!user) message.push('SMTP user not defined (N8N_SMTP_USER).');
if (!pass) message.push('SMTP pass not defined (N8N_SMTP_PASS).');
const message = []; throw message.length ? new Error(message.join(' '), { cause: error }) : error;
}
if (!host) message.push('SMTP host not defined (N8N_SMTP_HOST).');
if (!user) message.push('SMTP user not defined (N8N_SMTP_USER).');
if (!pass) message.push('SMTP pass not defined (N8N_SMTP_PASS).');
reject(new Error(message.length ? message.join(' ') : error.message));
});
});
} }
async sendMail(mailData: MailData): Promise<SendEmailResult> { async sendMail(mailData: MailData): Promise<SendEmailResult> {
@ -62,6 +55,7 @@ export class NodeMailer implements UserManagementMailerImplementation {
`Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`, `Email sent successfully to the following recipients: ${mailData.emailRecipients.toString()}`,
); );
} catch (error) { } catch (error) {
ErrorReporter.error(error);
Logger.error('Failed to send email', { recipients: mailData.emailRecipients, error }); Logger.error('Failed to send email', { recipients: mailData.emailRecipients, error });
return { return {
success: false, success: false,

View file

@ -1,7 +1,7 @@
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable import/no-cycle */ /* eslint-disable import/no-cycle */
import { Response } from 'express'; import { Response } from 'express';
import { LoggerProxy as Logger } from 'n8n-workflow'; import { ErrorReporterProxy as ErrorReporter, LoggerProxy as Logger } from 'n8n-workflow';
import { In } from 'typeorm'; import { In } from 'typeorm';
import validator from 'validator'; import validator from 'validator';
@ -159,6 +159,7 @@ export function usersNamespace(this: N8nApp): void {
public_api: false, public_api: false,
}); });
} catch (error) { } catch (error) {
ErrorReporter.error(error);
Logger.error('Failed to create user shells', { userShells: createUsers }); Logger.error('Failed to create user shells', { userShells: createUsers });
throw new ResponseHelper.ResponseError('An error occurred during user creation'); throw new ResponseHelper.ResponseError('An error occurred during user creation');
} }

View file

@ -6,8 +6,11 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { IRun, LoggerProxy as Logger, WorkflowOperationError } from 'n8n-workflow'; import {
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger,
WorkflowOperationError,
} from 'n8n-workflow';
import { FindManyOptions, LessThanOrEqual, ObjectLiteral } from 'typeorm'; import { FindManyOptions, LessThanOrEqual, ObjectLiteral } from 'typeorm';
import { DateUtils } from 'typeorm/util/DateUtils'; import { DateUtils } from 'typeorm/util/DateUtils';
@ -20,8 +23,6 @@ import {
IExecutionsStopData, IExecutionsStopData,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
ResponseHelper, ResponseHelper,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
WorkflowCredentials,
WorkflowRunner, WorkflowRunner,
} from '.'; } from '.';
import { getWorkflowOwner } from './UserManagement/UserManagementHelper'; import { getWorkflowOwner } from './UserManagement/UserManagementHelper';
@ -173,7 +174,8 @@ export class WaitTrackerClass {
// Start the execution again // Start the execution again
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
await workflowRunner.run(data, false, false, executionId); await workflowRunner.run(data, false, false, executionId);
})().catch((error) => { })().catch((error: Error) => {
ErrorReporter.error(error);
Logger.error( Logger.error(
`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`, `There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`,
{ executionId }, { executionId },

View file

@ -34,6 +34,7 @@ import {
IWebhookResponseData, IWebhookResponseData,
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger, LoggerProxy as Logger,
NodeHelpers, NodeHelpers,
Workflow, Workflow,
@ -434,6 +435,7 @@ export async function executeWebhook(
didSendResponse = true; didSendResponse = true;
}) })
.catch(async (error) => { .catch(async (error) => {
ErrorReporter.error(error);
Logger.error( Logger.error(
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`, `Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
{ executionId, workflowId: workflow.id }, { executionId, workflowId: workflow.id },

View file

@ -33,6 +33,7 @@ import {
import config from '../config'; import config from '../config';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { WEBHOOK_METHODS } from './WebhookHelpers'; import { WEBHOOK_METHODS } from './WebhookHelpers';
import { initErrorHandling } from './ErrorReporting';
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
require('body-parser-xml')(bodyParser); require('body-parser-xml')(bodyParser);
@ -217,6 +218,8 @@ class App {
this.presetCredentialsLoaded = false; this.presetCredentialsLoaded = false;
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint'); this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
initErrorHandling(this.app);
} }
/** /**

View file

@ -32,6 +32,7 @@ import {
IWorkflowExecuteHooks, IWorkflowExecuteHooks,
IWorkflowHooksOptionalParameters, IWorkflowHooksOptionalParameters,
IWorkflowSettings, IWorkflowSettings,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger, LoggerProxy as Logger,
SubworkflowOperationError, SubworkflowOperationError,
Workflow, Workflow,
@ -162,7 +163,8 @@ export function executeErrorWorkflow(
user, user,
); );
}) })
.catch((error) => { .catch((error: Error) => {
ErrorReporter.error(error);
Logger.error( Logger.error(
`Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`, `Could not execute ErrorWorkflow for execution ID ${this.executionId} because of error querying the workflow owner`,
{ {
@ -219,8 +221,9 @@ function pruneExecutionData(this: WorkflowHooks): void {
}, timeout * 1000), }, timeout * 1000),
) )
.catch((error) => { .catch((error) => {
throttling = false; ErrorReporter.error(error);
throttling = false;
Logger.error( Logger.error(
`Failed pruning execution data from database for execution ID ${this.executionId} (hookFunctionsSave)`, `Failed pruning execution data from database for execution ID ${this.executionId} (hookFunctionsSave)`,
{ {
@ -451,6 +454,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
flattenedExecutionData as IExecutionFlattedDb, flattenedExecutionData as IExecutionFlattedDb,
); );
} catch (err) { } catch (err) {
ErrorReporter.error(err);
// TODO: Improve in the future! // TODO: Improve in the future!
// Errors here might happen because of database access // Errors here might happen because of database access
// For busy machines, we may get "Database is locked" errors. // For busy machines, we may get "Database is locked" errors.
@ -511,6 +515,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
newStaticData, newStaticData,
); );
} catch (e) { } catch (e) {
ErrorReporter.error(e);
Logger.error( Logger.error(
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`, `There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (hookFunctionsSave)`,
{ executionId: this.executionId, workflowId: this.workflowData.id }, { executionId: this.executionId, workflowId: this.workflowData.id },
@ -629,6 +634,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
); );
} }
} catch (error) { } catch (error) {
ErrorReporter.error(error);
Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, { Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, {
executionId: this.executionId, executionId: this.executionId,
workflowId: this.workflowData.id, workflowId: this.workflowData.id,
@ -676,6 +682,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
newStaticData, newStaticData,
); );
} catch (e) { } catch (e) {
ErrorReporter.error(e);
Logger.error( Logger.error(
`There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`, `There was a problem saving the workflow with id "${this.workflowData.id}" to save changed staticData: "${e.message}" (workflowExecuteAfter)`,
{ sessionId: this.sessionId, workflowId: this.workflowData.id }, { sessionId: this.sessionId, workflowId: this.workflowData.id },

View file

@ -18,6 +18,7 @@ import {
IRun, IRun,
IRunExecutionData, IRunExecutionData,
ITaskData, ITaskData,
ErrorReporterProxy as ErrorReporter,
LoggerProxy as Logger, LoggerProxy as Logger,
NodeApiError, NodeApiError,
NodeOperationError, NodeOperationError,
@ -232,6 +233,7 @@ export async function executeErrorWorkflow(
const workflowRunner = new WorkflowRunner(); const workflowRunner = new WorkflowRunner();
await workflowRunner.run(runData); await workflowRunner.run(runData);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
Logger.error( Logger.error(
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`, `Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
{ workflowId: workflowErrorData.workflow.id }, { workflowId: workflowErrorData.workflow.id },
@ -407,9 +409,10 @@ export async function saveStaticData(workflow: Workflow): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
await saveStaticDataById(workflow.id!, workflow.staticData); await saveStaticDataById(workflow.id!, workflow.staticData);
workflow.staticData.__dataChanged = false; workflow.staticData.__dataChanged = false;
} catch (e) { } catch (error) {
ErrorReporter.error(error);
Logger.error( Logger.error(
`There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${e.message}"`, `There was a problem saving the workflow with id "${workflow.id}" to save changed staticData: "${error.message}"`,
{ workflowId: workflow.id }, { workflowId: workflow.id },
); );
} }

View file

@ -15,6 +15,7 @@
import { BinaryDataManager, IProcessMessage, WorkflowExecute } from 'n8n-core'; import { BinaryDataManager, IProcessMessage, WorkflowExecute } from 'n8n-core';
import { import {
ErrorReporterProxy as ErrorReporter,
ExecutionError, ExecutionError,
IDeferredPromise, IDeferredPromise,
IExecuteResponsePromiseData, IExecuteResponsePromiseData,
@ -56,6 +57,7 @@ import * as Queue from './Queue';
import { InternalHooksManager } from './InternalHooksManager'; import { InternalHooksManager } from './InternalHooksManager';
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper'; import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
import { generateFailedExecutionFromError } from './WorkflowHelpers'; import { generateFailedExecutionFromError } from './WorkflowHelpers';
import { initErrorHandling } from './ErrorReporting';
export class WorkflowRunner { export class WorkflowRunner {
activeExecutions: ActiveExecutions.ActiveExecutions; activeExecutions: ActiveExecutions.ActiveExecutions;
@ -76,6 +78,8 @@ export class WorkflowRunner {
if (executionsMode === 'queue') { if (executionsMode === 'queue') {
this.jobQueue = Queue.getInstance().getBullObjectInstance(); this.jobQueue = Queue.getInstance().getBullObjectInstance();
} }
initErrorHandling();
} }
/** /**
@ -98,6 +102,8 @@ export class WorkflowRunner {
executionId: string, executionId: string,
hooks?: WorkflowHooks, hooks?: WorkflowHooks,
) { ) {
ErrorReporter.error(error);
const fullRunData: IRun = { const fullRunData: IRun = {
data: { data: {
resultData: { resultData: {
@ -169,6 +175,7 @@ export class WorkflowRunner {
); );
}) })
.catch((error) => { .catch((error) => {
ErrorReporter.error(error);
console.error('There was a problem running internal hook "onWorkflowPostExecute"', error); console.error('There was a problem running internal hook "onWorkflowPostExecute"', error);
}); });
@ -182,6 +189,7 @@ export class WorkflowRunner {
]); ]);
}) })
.catch((error) => { .catch((error) => {
ErrorReporter.error(error);
console.error('There was a problem running hook "workflow.postExecute"', error); console.error('There was a problem running hook "workflow.postExecute"', error);
}); });
} }
@ -263,6 +271,7 @@ export class WorkflowRunner {
try { try {
await checkPermissionsForExecution(workflow, data.userId); await checkPermissionsForExecution(workflow, data.userId);
} catch (error) { } catch (error) {
ErrorReporter.error(error);
// Create a failed execution with the data for the node // Create a failed execution with the data for the node
// save it and abort execution // save it and abort execution
const failedExecution = generateFailedExecutionFromError( const failedExecution = generateFailedExecutionFromError(
@ -503,6 +512,7 @@ export class WorkflowRunner {
clearWatchdogInterval(); clearWatchdogInterval();
} }
} catch (error) { } catch (error) {
ErrorReporter.error(error);
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the // We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
// "workflowExecuteAfter" which we require. // "workflowExecuteAfter" which we require.
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter( const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(

View file

@ -9,6 +9,7 @@
import { BinaryDataManager, IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core'; import { BinaryDataManager, IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
import { import {
ErrorReporterProxy as ErrorReporter,
ExecutionError, ExecutionError,
ICredentialType, ICredentialType,
ICredentialTypeData, ICredentialTypeData,
@ -52,6 +53,7 @@ import { InternalHooksManager } from './InternalHooksManager';
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper'; import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
import { loadClassInIsolation } from './CommunityNodes/helpers'; import { loadClassInIsolation } from './CommunityNodes/helpers';
import { generateFailedExecutionFromError } from './WorkflowHelpers'; import { generateFailedExecutionFromError } from './WorkflowHelpers';
import { initErrorHandling } from './ErrorReporting';
export class WorkflowRunnerProcess { export class WorkflowRunnerProcess {
data: IWorkflowExecutionDataProcessWithExecution | undefined; data: IWorkflowExecutionDataProcessWithExecution | undefined;
@ -79,6 +81,10 @@ export class WorkflowRunnerProcess {
}, 30000); }, 30000);
} }
constructor() {
initErrorHandling();
}
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> { async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess); process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
process.on('SIGINT', WorkflowRunnerProcess.stopProcess); process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
@ -265,6 +271,7 @@ export class WorkflowRunnerProcess {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
await sendToParentProcess('sendMessageToUI', { source, message }); await sendToParentProcess('sendMessageToUI', { source, message });
} catch (error) { } catch (error) {
ErrorReporter.error(error);
this.logger.error( this.logger.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
`There was a problem sending UI data to parent process: "${error.message}"`, `There was a problem sending UI data to parent process: "${error.message}"`,
@ -402,6 +409,7 @@ export class WorkflowRunnerProcess {
parameters, parameters,
}); });
} catch (error) { } catch (error) {
ErrorReporter.error(error);
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error }); this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error });
} }
} }

View file

@ -256,7 +256,7 @@ export function getUserN8nFolderCustomExtensionPath(): string {
* have been downloaded * have been downloaded
* *
*/ */
export function getUserN8nFolderDowloadedNodesPath(): string { export function getUserN8nFolderDownloadedNodesPath(): string {
return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY); return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY);
} }

View file

@ -0,0 +1,36 @@
import type { Primitives } from './utils';
import * as Logger from './LoggerProxy';
export interface ReportingOptions {
level?: 'warning' | 'error';
tags?: Record<string, Primitives>;
extra?: Record<string, unknown>;
}
interface ErrorReporter {
report: (error: Error | string, options?: ReportingOptions) => void;
}
const isProduction = process.env.NODE_ENV === 'production';
const instance: ErrorReporter = {
report: (error, options) => isProduction && Logger.error('ERROR', { error, options }),
};
export function init(errorReporter: ErrorReporter) {
instance.report = errorReporter.report;
}
const wrap = (e: unknown) => {
if (e instanceof Error) return e;
if (typeof e === 'string') return new Error(e);
return;
};
export const error = (e: unknown, options?: ReportingOptions) => {
const toReport = wrap(e);
if (toReport) instance.report(toReport, options);
};
export const warn = (warning: Error | string, options?: ReportingOptions) =>
error(warning, { level: 'warning', ...options });

View file

@ -1,4 +1,5 @@
import * as LoggerProxy from './LoggerProxy'; import * as LoggerProxy from './LoggerProxy';
export * as ErrorReporterProxy from './ErrorReporterProxy';
import * as NodeHelpers from './NodeHelpers'; import * as NodeHelpers from './NodeHelpers';
import * as ObservableObject from './ObservableObject'; import * as ObservableObject from './ObservableObject';
import * as TelemetryHelpers from './TelemetryHelpers'; import * as TelemetryHelpers from './TelemetryHelpers';

View file

@ -1,5 +1,8 @@
import * as ErrorReporter from './ErrorReporterProxy';
export type Primitives = string | number | boolean | bigint | symbol | null | undefined;
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */
type Primitives = string | number | boolean | bigint | symbol | null | undefined;
export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string }) | Primitives>( export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string }) | Primitives>(
source: T, source: T,
hash = new WeakMap(), hash = new WeakMap(),
@ -16,6 +19,9 @@ export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string })
return source.toJSON() as T; return source.toJSON() as T;
} }
if (hash.has(source)) { if (hash.has(source)) {
ErrorReporter.warn('Circular reference detected', {
extra: { source, path },
});
return hash.get(source); return hash.get(source);
} }
// Array // Array