mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
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:
parent
0edd4bcc87
commit
41cb0eec6e
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
|
@ -14,6 +14,7 @@
|
|||
{
|
||||
"name": "Launch n8n with debug",
|
||||
"program": "${workspaceFolder}/packages/cli/bin/n8n",
|
||||
"cwd": "${workspaceFolder}/packages/cli/bin",
|
||||
"request": "launch",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "node",
|
||||
|
|
|
@ -4,6 +4,7 @@ ARG N8N_VERSION
|
|||
|
||||
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
||||
|
||||
ENV N8N_VERSION=${N8N_VERSION}
|
||||
RUN \
|
||||
apt-get update && \
|
||||
apt-get -y install graphicsmagick gosu git
|
||||
|
|
|
@ -4,6 +4,7 @@ ARG N8N_VERSION
|
|||
|
||||
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
|
||||
|
||||
ENV N8N_VERSION=${N8N_VERSION}
|
||||
RUN \
|
||||
yum install -y gcc-c++ make
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ FROM n8nio/base:${NODE_VERSION}
|
|||
ARG N8N_VERSION
|
||||
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
|
||||
RUN set -eux; \
|
||||
apkArch="$(apk --print-arch)"; \
|
||||
|
|
23167
package-lock.json
generated
23167
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -948,6 +948,15 @@ export const schema = {
|
|||
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: {
|
||||
doc: 'Diagnostics config for frontend.',
|
||||
format: String,
|
||||
|
|
|
@ -104,6 +104,8 @@
|
|||
"@oclif/core": "^1.9.3",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||
"@sentry/node": "^7.17.3",
|
||||
"@sentry/integrations": "^7.17.3",
|
||||
"axios": "^0.21.1",
|
||||
"basic-auth": "^2.0.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
WorkflowActivationError,
|
||||
WorkflowExecuteMode,
|
||||
LoggerProxy as Logger,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import express from 'express';
|
||||
|
@ -121,6 +122,7 @@ export class ActiveWorkflowRunner {
|
|||
});
|
||||
console.log(` => Started`);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
console.log(
|
||||
` => ERROR: Workflow could not be activated on first try, keep on trying`,
|
||||
);
|
||||
|
@ -881,6 +883,7 @@ export class ActiveWorkflowRunner {
|
|||
try {
|
||||
await this.add(workflowId, activationMode, workflowData);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
let lastTimeout = this.queuedWorkflowActivations[workflowId].lastTimeout;
|
||||
if (lastTimeout < WORKFLOW_REACTIVATE_MAX_TIMEOUT) {
|
||||
lastTimeout = Math.min(lastTimeout * 2, WORKFLOW_REACTIVATE_MAX_TIMEOUT);
|
||||
|
@ -948,6 +951,7 @@ export class ActiveWorkflowRunner {
|
|||
try {
|
||||
await this.removeWorkflowWebhooks(workflowId);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
console.error(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
`Could not remove webhooks of workflow "${workflowId}" because of error: "${error.message}"`,
|
||||
|
|
|
@ -76,7 +76,7 @@ export const executeCommand = async (
|
|||
command: string,
|
||||
options?: { doNotHandleError?: boolean },
|
||||
): Promise<string> => {
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
|
||||
const execOptions = {
|
||||
cwd: downloadFolder,
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
ITaskDataConnections,
|
||||
LoggerProxy as Logger,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
IHttpRequestHelper,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -672,6 +673,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
credentialsDecrypted,
|
||||
);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
// Do not fail any requests to allow custom error messages and
|
||||
// make logic easier
|
||||
if (error.cause?.response) {
|
||||
|
|
41
packages/cli/src/ErrorReporting.ts
Normal file
41
packages/cli/src/ErrorReporting.ts
Normal 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;
|
||||
};
|
|
@ -22,6 +22,7 @@ import {
|
|||
IVersionedNodeType,
|
||||
LoggerProxy,
|
||||
jsonParse,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -125,19 +126,23 @@ class LoadNodesAndCredentialsClass {
|
|||
const nodePackages = [];
|
||||
try {
|
||||
// Read downloaded nodes and credentials
|
||||
const downloadedNodesFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
||||
const downloadedNodesFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
const downloadedNodesFolderModules = path.join(downloadedNodesFolder, 'node_modules');
|
||||
await fsAccess(downloadedNodesFolderModules);
|
||||
const downloadedPackages = await this.getN8nNodePackages(downloadedNodesFolderModules);
|
||||
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) {
|
||||
try {
|
||||
await this.loadDataFromPackage(packagePath);
|
||||
// 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> {
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
const command = `npm install ${packageName}${version ? `@${version}` : ''}`;
|
||||
|
||||
await executeCommand(command);
|
||||
|
@ -285,7 +290,7 @@ class LoadNodesAndCredentialsClass {
|
|||
packageName: string,
|
||||
installedPackage: InstalledPackages,
|
||||
): Promise<InstalledPackages> {
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDowloadedNodesPath();
|
||||
const downloadFolder = UserSettings.getUserN8nFolderDownloadedNodesPath();
|
||||
|
||||
const command = `npm i ${packageName}@latest`;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { Request, Response } from 'express';
|
||||
import { parse, stringify } from 'flatted';
|
||||
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
|
@ -154,9 +155,13 @@ export function send<T, R extends Request, S extends Response>(
|
|||
|
||||
sendSuccessResponse(res, data, raw);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && isUniqueConstraintError(error)) {
|
||||
if (error instanceof Error) {
|
||||
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
|
||||
sendErrorResponse(res, error);
|
||||
|
|
|
@ -70,6 +70,7 @@ import {
|
|||
jsonParse,
|
||||
WebhookHttpMethod,
|
||||
WorkflowExecuteMode,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import basicAuth from 'basic-auth';
|
||||
|
@ -82,6 +83,7 @@ import parseUrl from 'parseurl';
|
|||
import promClient, { Registry } from 'prom-client';
|
||||
import history from 'connect-history-api-fallback';
|
||||
import bodyParser from 'body-parser';
|
||||
|
||||
import config from '../config';
|
||||
import * as Queue from './Queue';
|
||||
|
||||
|
@ -153,6 +155,7 @@ import glob from 'fast-glob';
|
|||
import { ResponseError } from './ResponseHelper';
|
||||
|
||||
import { toHttpNodeParameters } from './CurlConverterHelper';
|
||||
import { initErrorHandling } from './ErrorReporting';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -256,6 +259,8 @@ class App {
|
|||
this.presetCredentialsLoaded = false;
|
||||
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||
|
||||
initErrorHandling(this.app);
|
||||
|
||||
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
||||
const telemetrySettings: ITelemetrySettings = {
|
||||
enabled: config.getEnv('diagnostics.enabled'),
|
||||
|
@ -742,6 +747,7 @@ class App {
|
|||
// DB ping
|
||||
await connection.query('SELECT 1');
|
||||
} catch (err) {
|
||||
ErrorReporter.error(err);
|
||||
LoggerProxy.error('No Database connection!', err);
|
||||
const error = new ResponseHelper.ResponseError('No Database connection!', undefined, 503);
|
||||
return ResponseHelper.sendErrorResponse(res, error);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
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 { MailData, SendEmailResult, UserManagementMailerImplementation } from './Interfaces';
|
||||
|
||||
|
@ -24,22 +24,15 @@ export class NodeMailer implements UserManagementMailerImplementation {
|
|||
const user = config.getEnv('userManagement.emails.smtp.auth.user');
|
||||
const pass = config.getEnv('userManagement.emails.smtp.auth.pass');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.transport.verify((error: Error) => {
|
||||
if (!error) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = [];
|
||||
|
||||
try {
|
||||
await this.transport.verify();
|
||||
} catch (error) {
|
||||
const message: string[] = [];
|
||||
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));
|
||||
});
|
||||
});
|
||||
throw message.length ? new Error(message.join(' '), { cause: error }) : error;
|
||||
}
|
||||
}
|
||||
|
||||
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()}`,
|
||||
);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error('Failed to send email', { recipients: mailData.emailRecipients, error });
|
||||
return {
|
||||
success: false,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable import/no-cycle */
|
||||
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 validator from 'validator';
|
||||
|
||||
|
@ -159,6 +159,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
public_api: false,
|
||||
});
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error('Failed to create user shells', { userShells: createUsers });
|
||||
throw new ResponseHelper.ResponseError('An error occurred during user creation');
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
// 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 { DateUtils } from 'typeorm/util/DateUtils';
|
||||
|
@ -20,8 +23,6 @@ import {
|
|||
IExecutionsStopData,
|
||||
IWorkflowExecutionDataProcess,
|
||||
ResponseHelper,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowRunner,
|
||||
} from '.';
|
||||
import { getWorkflowOwner } from './UserManagement/UserManagementHelper';
|
||||
|
@ -173,7 +174,8 @@ export class WaitTrackerClass {
|
|||
// Start the execution again
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
await workflowRunner.run(data, false, false, executionId);
|
||||
})().catch((error) => {
|
||||
})().catch((error: Error) => {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error(
|
||||
`There was a problem starting the waiting execution with id "${executionId}": "${error.message}"`,
|
||||
{ executionId },
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
IWebhookResponseData,
|
||||
IWorkflowDataProxyAdditionalKeys,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
LoggerProxy as Logger,
|
||||
NodeHelpers,
|
||||
Workflow,
|
||||
|
@ -434,6 +435,7 @@ export async function executeWebhook(
|
|||
didSendResponse = true;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error(
|
||||
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
|
||||
{ executionId, workflowId: workflow.id },
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
import config from '../config';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { WEBHOOK_METHODS } from './WebhookHelpers';
|
||||
import { initErrorHandling } from './ErrorReporting';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-call
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
@ -217,6 +218,8 @@ class App {
|
|||
|
||||
this.presetCredentialsLoaded = false;
|
||||
this.endpointPresetCredentials = config.getEnv('credentials.overwrite.endpoint');
|
||||
|
||||
initErrorHandling(this.app);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
IWorkflowExecuteHooks,
|
||||
IWorkflowHooksOptionalParameters,
|
||||
IWorkflowSettings,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
LoggerProxy as Logger,
|
||||
SubworkflowOperationError,
|
||||
Workflow,
|
||||
|
@ -162,7 +163,8 @@ export function executeErrorWorkflow(
|
|||
user,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: Error) => {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error(
|
||||
`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),
|
||||
)
|
||||
.catch((error) => {
|
||||
throttling = false;
|
||||
ErrorReporter.error(error);
|
||||
|
||||
throttling = false;
|
||||
Logger.error(
|
||||
`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,
|
||||
);
|
||||
} catch (err) {
|
||||
ErrorReporter.error(err);
|
||||
// TODO: Improve in the future!
|
||||
// Errors here might happen because of database access
|
||||
// For busy machines, we may get "Database is locked" errors.
|
||||
|
@ -511,6 +515,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
newStaticData,
|
||||
);
|
||||
} catch (e) {
|
||||
ErrorReporter.error(e);
|
||||
Logger.error(
|
||||
`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 },
|
||||
|
@ -629,6 +634,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
);
|
||||
}
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, {
|
||||
executionId: this.executionId,
|
||||
workflowId: this.workflowData.id,
|
||||
|
@ -676,6 +682,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||
newStaticData,
|
||||
);
|
||||
} catch (e) {
|
||||
ErrorReporter.error(e);
|
||||
Logger.error(
|
||||
`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 },
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
IRun,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
LoggerProxy as Logger,
|
||||
NodeApiError,
|
||||
NodeOperationError,
|
||||
|
@ -232,6 +233,7 @@ export async function executeErrorWorkflow(
|
|||
const workflowRunner = new WorkflowRunner();
|
||||
await workflowRunner.run(runData);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
Logger.error(
|
||||
`Calling Error Workflow for "${workflowErrorData.workflow.id}": "${error.message}"`,
|
||||
{ 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
|
||||
await saveStaticDataById(workflow.id!, workflow.staticData);
|
||||
workflow.staticData.__dataChanged = false;
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
ErrorReporter.error(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 },
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { BinaryDataManager, IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import {
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
ExecutionError,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
|
@ -56,6 +57,7 @@ import * as Queue from './Queue';
|
|||
import { InternalHooksManager } from './InternalHooksManager';
|
||||
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
|
||||
import { generateFailedExecutionFromError } from './WorkflowHelpers';
|
||||
import { initErrorHandling } from './ErrorReporting';
|
||||
|
||||
export class WorkflowRunner {
|
||||
activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||
|
@ -76,6 +78,8 @@ export class WorkflowRunner {
|
|||
if (executionsMode === 'queue') {
|
||||
this.jobQueue = Queue.getInstance().getBullObjectInstance();
|
||||
}
|
||||
|
||||
initErrorHandling();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,6 +102,8 @@ export class WorkflowRunner {
|
|||
executionId: string,
|
||||
hooks?: WorkflowHooks,
|
||||
) {
|
||||
ErrorReporter.error(error);
|
||||
|
||||
const fullRunData: IRun = {
|
||||
data: {
|
||||
resultData: {
|
||||
|
@ -169,6 +175,7 @@ export class WorkflowRunner {
|
|||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
ErrorReporter.error(error);
|
||||
console.error('There was a problem running internal hook "onWorkflowPostExecute"', error);
|
||||
});
|
||||
|
||||
|
@ -182,6 +189,7 @@ export class WorkflowRunner {
|
|||
]);
|
||||
})
|
||||
.catch((error) => {
|
||||
ErrorReporter.error(error);
|
||||
console.error('There was a problem running hook "workflow.postExecute"', error);
|
||||
});
|
||||
}
|
||||
|
@ -263,6 +271,7 @@ export class WorkflowRunner {
|
|||
try {
|
||||
await checkPermissionsForExecution(workflow, data.userId);
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
// Create a failed execution with the data for the node
|
||||
// save it and abort execution
|
||||
const failedExecution = generateFailedExecutionFromError(
|
||||
|
@ -503,6 +512,7 @@ export class WorkflowRunner {
|
|||
clearWatchdogInterval();
|
||||
}
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
// We use "getWorkflowHooksWorkerExecuter" as "getWorkflowHooksWorkerMain" does not contain the
|
||||
// "workflowExecuteAfter" which we require.
|
||||
const hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import { BinaryDataManager, IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import {
|
||||
ErrorReporterProxy as ErrorReporter,
|
||||
ExecutionError,
|
||||
ICredentialType,
|
||||
ICredentialTypeData,
|
||||
|
@ -52,6 +53,7 @@ import { InternalHooksManager } from './InternalHooksManager';
|
|||
import { checkPermissionsForExecution } from './UserManagement/UserManagementHelper';
|
||||
import { loadClassInIsolation } from './CommunityNodes/helpers';
|
||||
import { generateFailedExecutionFromError } from './WorkflowHelpers';
|
||||
import { initErrorHandling } from './ErrorReporting';
|
||||
|
||||
export class WorkflowRunnerProcess {
|
||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||
|
@ -79,6 +81,10 @@ export class WorkflowRunnerProcess {
|
|||
}, 30000);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
initErrorHandling();
|
||||
}
|
||||
|
||||
async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
|
||||
process.on('SIGTERM', WorkflowRunnerProcess.stopProcess);
|
||||
process.on('SIGINT', WorkflowRunnerProcess.stopProcess);
|
||||
|
@ -265,6 +271,7 @@ export class WorkflowRunnerProcess {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
await sendToParentProcess('sendMessageToUI', { source, message });
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
this.logger.error(
|
||||
// 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}"`,
|
||||
|
@ -402,6 +409,7 @@ export class WorkflowRunnerProcess {
|
|||
parameters,
|
||||
});
|
||||
} catch (error) {
|
||||
ErrorReporter.error(error);
|
||||
this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ export function getUserN8nFolderCustomExtensionPath(): string {
|
|||
* have been downloaded
|
||||
*
|
||||
*/
|
||||
export function getUserN8nFolderDowloadedNodesPath(): string {
|
||||
export function getUserN8nFolderDownloadedNodesPath(): string {
|
||||
return path.join(getUserN8nFolderPath(), DOWNLOADED_NODES_SUBDIRECTORY);
|
||||
}
|
||||
|
||||
|
|
36
packages/workflow/src/ErrorReporterProxy.ts
Normal file
36
packages/workflow/src/ErrorReporterProxy.ts
Normal 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 });
|
|
@ -1,4 +1,5 @@
|
|||
import * as LoggerProxy from './LoggerProxy';
|
||||
export * as ErrorReporterProxy from './ErrorReporterProxy';
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
import * as ObservableObject from './ObservableObject';
|
||||
import * as TelemetryHelpers from './TelemetryHelpers';
|
||||
|
|
|
@ -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 */
|
||||
type Primitives = string | number | boolean | bigint | symbol | null | undefined;
|
||||
export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string }) | Primitives>(
|
||||
source: T,
|
||||
hash = new WeakMap(),
|
||||
|
@ -16,6 +19,9 @@ export const deepCopy = <T extends ((object | Date) & { toJSON?: () => string })
|
|||
return source.toJSON() as T;
|
||||
}
|
||||
if (hash.has(source)) {
|
||||
ErrorReporter.warn('Circular reference detected', {
|
||||
extra: { source, path },
|
||||
});
|
||||
return hash.get(source);
|
||||
}
|
||||
// Array
|
||||
|
|
Loading…
Reference in a new issue